Как создать эффект бликов на линзах — подробный туториал

Для всех, кто хочет повторить впечатляющие блики из Cyberpunk 2077.

Блики на линзах — это фотографический артефакт, который возникает при взаимодействии объектива и света. Этот эффект также встречается в играх — разработчики сознательно добавляют его по нескольким причинам:

  • он увеличивает яркость и динамический диапазон изображения;
  • блики усиливают погружение, так как позволяют ощутить «физичность» камеры;
  • эффект может играть важную стилистическую или драматическую роль. Также его можно использовать в качестве игровой механики: к примеру, блики могут ослеплять игрока.

Разработчик Джон Чепмен в 2013 году опубликовал в своём блоге подробный туториал, в котором рассказал, как реализовать эффект бликов в игре. Автор поделился нужным кодом, а также дал описание к каждому этапу создания бликов. Мы выбрали из текста главное.

Поэтапное создание эффекта бликов

Традиционно для создания бликов применяют спрайты, так как их легко настраивать, а также они выглядят достаточно реалистично. Но для их корректной работы нужно использовать occlusion-данные, что может усложнить задачу. Чепмен предложил иной подход, который не даёт таких же реалистичных результатов, но может частично заменить спрайты.

На первом этапе важно определить самые яркие пиксели на оригинальном изображении, чтобы на их основе сделать блики. Также нужно использовать даунсемплинг для оптимизации. Самый простой и гибкий способ — применить 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; }
Результат первого этапа: мы получили изображение низкого разрешения, на котором отмечены самые яркие пиксели 
Результат первого этапа: мы получили изображение низкого разрешения, на котором отмечены самые яркие пиксели 

Эффект можно настроить с помощью параметров scale и bias: результат сильно зависит от динамического диапазона, а также интенсивности бликов. Следите за тем, чтобы они получились не слишком яркими.

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

vec2 texcoord = -vTexcoords + vec2(1.0);

Следующий шаг: создание «призраков» — повторяющихся бликов, которые появляются с противоположной стороны относительно центра изображения. Чтобы сделать их, нужно взять вектор, идущий от пикселя к центру экрана, а затем получить несколько сэмплов вдоль этого вектора.

Принцип появления «призраков»
Принцип появления «призраков»
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; }
Блики с «призраками»
Блики с «призраками»

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

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; }

Функция веса достаточно проста — она выглядит как линейное убывание. Вес используется только внутри области сэмплов. Это нужно, чтобы «призраки» создавались только от тех источников света, которые находятся в центре изображения.

Слева — визуализация области, в которой работает вес сэмплов. Справа — результат визуализации «призраков»
Слева — визуализация области, в которой работает вес сэмплов. Справа — результат визуализации «призраков»
Также эффект можно улучшить, если изменить цвет «призраков» при помощи 1D-текстуры. Слева — радиальная 1D-текстура, справа — результат с окрашенными «призраками»
Также эффект можно улучшить, если изменить цвет «призраков» при помощи 1D-текстуры. Слева — радиальная 1D-текстура, справа — результат с окрашенными «призраками»

Окрашивание применяется в последнюю очередь, чтобы повлиять на финальный цвет «призраков».

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

Следующий шаг — создание эффекта гало. Если взять вектор к центру изображения, но зафиксировать длину вектора, то получится нужный эффект — исходное изображение будет радиально искривлено.

Результат можно использовать для создания гало
Результат можно использовать для создания гало

Чтобы создать эффект гало, нужно определить область веса, как в случае с «призраками». Только на этот раз область должна быть не в центре изображения, а по краям. Радиус получившегося овального кольца контролируется при помощи 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;
Слева — область веса для гало. Справа — результат
Слева — область веса для гало. Справа — результат

Ещё одна часть эффекта — хроматические искажения. В реальности у некоторых бликов заметны хроматические искажения, которые появляются из-за того, что волны света разной длины преломляются по-разному. Эту особенность можно смоделировать, если создать функцию, которая разделяет красный, зелёный и синий каналы, а потом немного смещает каждый из них вдоль вектора сэмплинга.

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 ); }

Чепмен использовал следующие параметры направления и искажения:

vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0)); vec3 distortion = vec3(-texelSize.x * uDistortion, 0.0, texelSize.x * uDistortion); vec3 direction = normalize(ghostVec);
Это промежуточный результат, в котором реализованы все ключевые особенности эффекта
Это промежуточный результат, в котором реализованы все ключевые особенности эффекта

Сейчас блики получились слишком чёткими. Чтобы исправить это, нужно добавить эффект blur. Чепмен не стал подробно рассказывать об этом, но указал ссылку на туториал.

Слева — до blur, справа — после
Слева — до blur, справа — после

Теперь нужно смешать полученный эффект с исходным изображением. Но нужно учитывать две вещи. Во-первых, эффекты motion blur или depth of field необходимо добавить до объединения бликов с исходным изображением. Это значит, что постобработка не будет накладываться на блики. Во-вторых, если вы собираетесь использовать tonemapping, то добавьте блики перед объединением, так как этот эффект должен распространяться и на них.

Чтобы сделать эффект более реалистичным, можно добавить текстуру грязи.

Слева — блики и текстура грязи. Справа — результат совмещения
Слева — блики и текстура грязи. Справа — результат совмещения
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; }

Если у текстуры грязи низкий контраст, то блики будут сильно выделяться. Если увеличить его, то блики станут более приглушёнными.

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

Слева — блики, грязь и текстура 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 вокруг центра изображения.

В результате получился сравнительно простой способ создания эффектов бликов от линз, который строится на основе постобработки. Результат нельзя назвать полностью фотореалистичным, но при должной настройке он может выглядеть отлично.

Как создать эффект бликов на линзах — подробный туториал
Подобный эффект можно увидеть в Cyberpunk 2077. Вы сможете повторить его с помощью этого туториала. Источник видео
158158
44 комментария

Создавать блики нужно как минимум для того, чтобы потом была возможность их потом сразу же отключить в настройках.
Кто не согласен, того приглашаю на скриншот-дуэль

45
Ответить

Чем плохи блики? Это не зернистость или абберация которая картинку мылит. Наоборот эффект картинке придаёт 

25
Ответить

скриншот-дуэльДавай по другому

Ответить

Их нужно грамотно делать, как и любую часть в графики)

Ответить

Ну и да, я так экспериментировал с RBDOOM 3, результат так себе. Надо допиливать. Туториал этот "протух" немного, есть обновлённая версия. 

3
Ответить