Привет! Представляю вашему вниманию перевод статьи «Dynamic Local Exposure» автора John Chapman.
В данной статье я представлю пару идей о динамической локальной экспозиции в 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 уровням luminance buffer (4).
float Lavg = exp(textureLod(txLuminance, uv, uLuminanceLod).x;
Обратите внимание, для того, чтобы это сработало, гистерезис следует применять только на последнем шаге (при записи 1x1 mip уровня), в противном случае будут артефакты.
В теории это отличная идея: каждая область изображения может иметь хорошую экспозицию, при этом быть в контрасте с соседними областями. Однако, на практике получается отвратительный результат:
Наиболее неприятными являются блочные “ореолы”, которые встречаются в областях с высокой контрастностью:
Однако их можно сгладить либо предварительной фильтрацией luminance buffer, либо просто с помощью бикубического сэмплинга:
Все еще выглядит отвратительно, но уже лучше.
Сэмплинг различных уровней mipmap у luminance меняет радиус ореола. Этот параметр полезен для контроля общего “внешнего вида” результата, а также для минимизации эффекта ореола, хотя и за счет общего уменьшения контраста (он становится фильтром границ) или потери локальности контроля экспозиции:
Все же сглаживания ореолов недостаточно. Результат вообще не естественный; выглядит как 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
Изменяя коэффициент смешивания, можно настроить локальную экспозицию так, чтобы в результате минимизировать артефакты и максимизировать воспринимаемый реализм:
Автоматический коэффициент смешивания
Настройка коэффициента смешивания вручную нормальна в ситуациях, где у нас есть абсолютный контроль позиции камеры, освещения и т.д. Однако во многих случаях (например, игры под открытым небом с динамической сменой дня и ночи) этот уровень контроля попросту невозможен. В данном случае было бы неплохо генерировать коэффициент смешивания автоматически.
На изображении ниже у нас широкий динамический диапазон; в основном средне-низкие значения яркости и несколько областей с высокой интенсивностью (небо в окнах):
Без локальной экспозиции цвет неба теряется. В этом случае хотелось бы большой коэффициент смешивания:
Теперь рассмотрим изображение ниже, которое имеет небольшой динамический диапазон в основном с высоким значением яркости:
В этом случае применение локальной экспозиции слишком сильно уменьшает яркость “ярких” областей:
Данные наблюдения намекают на простой метод смешивания локальных и глобальных значений: если разница между средней и максимальной яркостью сцены больше, то и коэффициент смешивания локальной экспозиции должен быть больше. Генерацию максимальной яркости сцены можно сделать тривиально во время расчета яркости, применяя гистерезис для сглаживания результата тем же способом, что и для среднего значения. Поэтому мы можем расширить предыдущий фрагмент кода следующим образом:
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. Это означает, что измерение яркости нужно сделать непосредственно перед применением экспозиции. Если это неприемлемо, то решением будет генерация локальной яркости отдельно от среднего и максимального значений.
Хотя я думаю, что использование локальной и глобальной яркостей сцены вместе является “верным” подходом к созданию сбалансированного, естественно выглядящего изображения, качество результата, очевидно, субъективно. Подходит ли подобный метод к конкретной игре, полностью зависит от контента и желаемого визуального стиля. Мне было бы интересно услышать и другие идеи на этот счет.
Ссылки
- Localized Tonemapping (Bart Wronski)
- Implementing a Physically Based Camera (Padraic Hennessy)
- Moving Frostbite to PBR (Sébastien Lagarde, et al.)
- A Closer Look at Tonemapping (Matt Pettineo)
- The Importance of Being Linear (Larry Gritz, et al.)
- Advanced Techniques and Optimization of HDR/VDR Color Pipelines (Timothy Lottes)
HDR изображения взяты из sIBL Archive.
Автор: migom