Динамическая локальная экспозиция

https://john-chapman.github.io/2017/08/23/dynamic-local-exposure.html
  • Перевод
Привет, Хабр! Представляю вашему вниманию перевод статьи «Dynamic Local Exposure» автора John Chapman.

image

В данной статье я представлю пару идей о динамической локальной экспозиции в HDR рендеринге. У Барта Вронски уже есть отличная статья на эту тему и я очень рекомендую ее прочитать прямо сейчас, если вы еще этого не сделали; идеи здесь, в большей степени, основаны на его статье. В конце я включил несколько других замечательных ссылок.

Low/High Dynamic Range


В старые добрые времена (1990-е) игры рендерились непосредственно в отображаемом LDR (узкий динамический диапазон) формате (гамма пространство, 8 бит). Это было просто и дешево, но, с другой стороны, значительно мешало созданию действительно фотореалистичной картинки.

В настоящее время, особенно с появлением PBR (physically-based rendering), игры рендерятся с гигантским динамическим диапазоном в линейном пространстве с более высокой точностью. С таким движением к фотореализму приходит реальная проблема: как мы можем отобразить HDR изображение в LDR?

Глобальная автоэкспозиция


Стандартный подход к автоматическому контролю экспозиции заключается в измерении средней (или средней логарифмической) яркости сцены, опционально с weight функцией, отдающей предпочтение значениям, близким к центру изображения. Это можно сделать очень эффективно с помощью параллельного уменьшения или путем многократного downsampling в mipmap у luminance buffer (буфер яркости). Последний подход имеет некоторые преимущества, о которых я расскажу в следующем разделе.

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

float Lavg = exp(textureLod(txLuminance, uv, 99.0).x);
float ev100 = log2(Lavg * 100.0 / 12.5);
ev100 -= uExposureCompensation; // optional manual bias 
float exposure = 1.0 / (1.2 * exp2(ev100));

Получено из стандарта ISO расчета скорости на основе насыщения, полное объяснение см. в (3)

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

Lavg = Lavg + (Lnew - Lavg) * (1.0 - exp(uDeltaTime * -uRate));

Комментарий переводчика
Данную функцию нужно применять в шейдере downsampling текстуры яркости и только во время расчета последнего mip уровня (1x1). Далее про это будет написано, но на мой взгляд это легко упустить из виду.

Из-за своей глобальной природы, этот метод страдает от сильных затенений либо засветов областей изображения, в которых есть отклонение от средней яркости:

image

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

Локальная автоэкспозиция


Если мы генерируем среднюю яркость при помощи downsampling, для получения локальной средней яркости у нас есть доступ к более низким mip уровням luminance buffer (4).

float Lavg = exp(textureLod(txLuminance, uv, uLuminanceLod).x;

Обратите внимание, для того, чтобы это сработало, гистерезис следует применять только на последнем шаге (при записи 1x1 mip уровня), в противном случае будут артефакты.

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

image

Наиболее неприятными являются блочные “ореолы”, которые встречаются в областях с высокой контрастностью:

image

Однако их можно сгладить либо предварительной фильтрацией luminance buffer, либо просто с помощью бикубического сэмплинга:

image

Все еще выглядит отвратительно, но уже лучше.

Сэмплинг различных уровней mipmap у luminance меняет радиус ореола. Этот параметр полезен для контроля общего “внешнего вида” результата, а также для минимизации эффекта ореола, хотя и за счет общего уменьшения контраста (он становится фильтром границ) или потери локальности контроля экспозиции:

image

Все же сглаживания ореолов недостаточно. Результат вообще не естественный; выглядит как extreme “HDR photo” style, в отличие от того, что видит человек. Однако смешивая глобальное и локальное значения, мы можем получить лучшее из обоих миров:

float Llocal  = exp(textureLod(txLuminance, uv, uLuminanceLod).x;
float Lglobal = exp(textureLod(txLuminance, uv, 99.0).x;
float L       = mix(Lglobal, Llocal, uLocalExposureRatio);
// .. use L to compute the final exposure scale as before

image

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

image

Автоматический коэффициент смешивания


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

На изображении ниже у нас широкий динамический диапазон; в основном средне-низкие значения яркости и несколько областей с высокой интенсивностью (небо в окнах):

image

Без локальной экспозиции цвет неба теряется. В этом случае хотелось бы большой коэффициент смешивания:

image

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

image

В этом случае применение локальной экспозиции слишком сильно уменьшает яркость “ярких” областей:

image

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

float Llocal  = exp(textureLod(txLuminance, uv, uLuminanceLod).x;
float Lglobal = exp(textureLod(txLuminance, uv, 99.0).x; // average in x
float Lmax    = exp(textureLod(txLuminance, uv, 99.0).y; // max in y
float Lratio  = min(saturate(abs(Lmax - Lglobal) / Lmax), uLocalExposureMax);
float L       = mix(Lglobal, Llocal, Lratio);
// .. use L to compute the final exposure scale as before

Обратите внимание, что у нас на вход появился uLocalExposureMax для контроля абсолютной максимальной степени влияния локальной экспозиции. У меня хороший результат дал uLocalExposureMax < 0.3.

Финальный код
float Llocal  = exp(textureLod(txLuminance, uv, uLuminanceLod).x;
float Lglobal = exp(textureLod(txLuminance, uv, 99.0).x; // average in x
float Lmax    = exp(textureLod(txLuminance, uv, 99.0).y; // max in y
float Lratio  = min(saturate(abs(Lmax - Lglobal) / Lmax), uLocalExposureMax);
float L       = mix(Lglobal, Llocal, Lratio);

float ev100 = log2(L * 100.0 / 12.5);
ev100 -= uExposureCompensation; // optional manual bias 
float exposure = 1.0 / (1.2 * exp2(ev100));

vec3 result = hdrColor * exposure;
result += bloom;
//etc

outColor.rgb = result;


Заключение


Подход, изложенный выше, накладывает некоторые ограничения на то, когда нужно измерять яркость сцены. Обычно измерение выполняется сразу после прохода освещения, чтобы избежать адаптации particle эффектов, bloom и т.д. Однако, когда используется локальная яркость важно, чтобы настоящее значение, которое участвует в экспозиции, было представлено в luminance map. Это означает, что измерение яркости нужно сделать непосредственно перед применением экспозиции. Если это неприемлемо, то решением будет генерация локальной яркости отдельно от среднего и максимального значений.

Хотя я думаю, что использование локальной и глобальной яркостей сцены вместе является “верным” подходом к созданию сбалансированного, естественно выглядящего изображения, качество результата, очевидно, субъективно. Подходит ли подобный метод к конкретной игре, полностью зависит от контента и желаемого визуального стиля. Мне было бы интересно услышать и другие идеи на этот счет.

Ссылки


  1. Localized Tonemapping (Bart Wronski)
  2. Implementing a Physically Based Camera (Padraic Hennessy)
  3. Moving Frostbite to PBR (Sébastien Lagarde, et al.)
  4. A Closer Look at Tonemapping (Matt Pettineo)
  5. The Importance of Being Linear (Larry Gritz, et al.)
  6. Advanced Techniques and Optimization of HDR/VDR Color Pipelines (Timothy Lottes)

HDR изображения взяты из sIBL Archive.
  • +22
  • 1,9k
  • 3
Поделиться публикацией

Комментарии 3

    0

    Простой метод и красивый результат.

      0
      Однако смешивая глобальное и локальное значения, мы можем получить лучшее из обоих миров
      В корне не согласен с автором оригинальной статьи, так как это, по сути, наложение всё того же, «плохого» эффекта, но с определенной прозрачностью, что не отменяет того, что он имеет ореолы и не перестает быть в основе своей тем самым extreme “HDR photo” style.
      Ореолы воспринимаются неестественно по той причине, что человеческая зрительная система и мозг понимают, что какой-то отдельный объект, освещен «как-то не так», неравномерно, то есть задействованы некоторые иерархические свойства сцены, которые в «живом» фото еще необходимо извлечь из картинки, а вот в игровых движках они, как правило, идут «из коробки».
      Критикуешь — предлагай!
      Чем ближе в «иерархическом дереве» игровой сцены (не знаю, как правильно это называется, могу быть неточен в терминах, за что прошу меня простить) находятся объекты, тем меньше должна быть разница в уровне экспокоррекции между ними, а внутри объектов она должна отсутствовать, т.е. коррекция должна применяться ко всему объекту целиком.
      На примере последней фотографии можно выделить три части: небо, здание и трава с деревьями. Каждой части должен быть присвоен свой уровень экспокоррекции и, возможно, коррекции контраста, это избавит ситуацию как от проблемы ореолов, так и от других артефактов.
        0
        Да, согласен, проблема с «ореолами» останется, но не будет настолько заметной после смешивания.
        Автор и не пишет, что проблема полностью разрешиться алгоритмом, множество параметров у него подбирается вручную и передаются в качестве uniform, экспокоррекция в том числе (uExposureCompensation)
        Хотя на самом деле ее стоит рассчитывать. Есть отличная статья на эту тему
        knarkowicz.wordpress.com/2016/01/09/automatic-exposure
        В ней рассказывается, что экспокоррекцию рассчитывают из L по формуле, либо зависимость настаивается художниками и функция храниться в 1D текстуре. Второй вариант предпочтительнее

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое