Сергей Макеев (joker_ru) wrote,
Сергей Макеев
joker_ru

Быстрый HDR, используя только LDR текстуры

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

Будем рассуждать логически, в нашем мире обычные фотоаппараты “мыльницы” - как раз имеют ярко выраженный эффект, который в играх называют HDR. Засвечивают очень яркие области или и полностью затемняют очень темные. При этом если направлять камеру с ярких областей на темные, то отлично видно эффект адаптации “глаза” к освещенности. При этом мыльницы получают с матрицы ну максимум по 12 бит на канал, а старые по 8. Как же они это делают?

Камера постоянно производит матричный замер экспозиции, используя одно или несколько значений с матрицы, оценивая таким образом общую яркость/контрастность кадра. И используя полученное значение, в зависимости от заданного режима съемки производит коррекцию экспозиции в нужную сторону. Таким образом, камера итеративно подбирает нужное значение экспозиции. Это и есть та самая нужная нам, “адаптация глаза к свету”, которую в играх выдают за HDR эффект.

Что же мешает делать нам тоже самое, а не использовать дорогой рендер в рендертаргет с плавающей точкой? Правильно, ничего не мешает - давайте попробуем.

Итак алгоритм следующий:

  • Рисуем в рендертаргет R8G8B8(X8) все что захотим, с одним отличием - перед тем как вернуть финальный цвет из пиксельшейдера умножаем его коэфицент яркости (Exposure Adjustment)
  • После того, как кадр нарисован, производим оценку яркости кадра (основная хитрость как сделать это быстро, об этом ниже)
  • В зависимости от посчитанной яркости кадра изменяем Exposure Adjustment, если кадр слишком темный, повышаем наш множитель. Если слишком светлый, понижаем.
  • Далее, повторяем все заново.

Как видно, алгоритм очень простой. Но нам нужно оценивать яркость отрисованного кадра, для правильного сдвига Exposure Adjustment. Обычно, для оценки яркости кадра используется гистограмма, но мы же хотим сделать быстрый HDR и желательно без использования CPU, а классическая гистограмма для этого не лучший вариант.

Вот мой вариант быстрого экспозамера кадра на GPU:

  • Копируем полученный кадр в квадратную текстуру, меньшего размера чем экран, при этом производим оценку яркости каждого пикселя следующим образом: считаем яркость пикселя и в завсисмости от яркости в разные каналы RGB кладем разные значения. В R канал кладем коэфицент, что пиксель недосвечен, в B канал кладем коэфицент, что пиксель пересвечен, в G канал кладем нормализованный остаток: 1.0 - R - G. Таким образом, полностью черный пиксель превратится во float3(1, 0, 0) а полностью белый во float3(0, 0, 1). Сумма 3-х компонентов RGB всегда нормализованна и равна 1.0. Фактически это оценка данного пикселя 3 функциями dark, medium и light colors (гистограмма с 3 корзинами). Таким способом, мы получили оценку каждого пикселя отдельно, но нам нужна оценка всей картинки вцелом.
  • Входные данные при предыдущем шаге сформированны таким образом, что теперь оценку освещенности для всего кадра, можно сделать очень быстро. Рисуем получившуюся текстуру с коэфицентами, в текстуру с разрешением в два раза меньше (в следующий mip). При этом используем LINEAR фильтрацию текстур и делаем каждую нашу выборку между текселями. GPU при этом автоматически сэмплирует 4 текселя и т.к. мы используем LINEAR фильтрацию фактически проведет нормализацию результата (tex1 + tex2 + tex3 + tex4 * 0.25). Результат: оценка яркостной составляющей блока пикселей 2×2. Повторяем шаг до размера размера текстуры 2×2


  • Теперь у нас есть гистограмма всей картинки (правда всего на 3 корзины) и нам нужно сдвинуть нашу коррекцию экспозиции. Т.к. зачитывать данные от GPU к CPU не хочется (данных хоть и мало, но синхронизация GPU с CPU это вообще всегда медленно), заводим текстуру Exposure Adjustment размером 1×1 пиксель (по идее надо float текстуру заводить, но я попробовал хватает точности и у обычной 8 битной). Данная текстура всегда будет лежать в текстурном кеше GPU и фактически является константой. Эту текстуру и используем как множитель Exposure Adjustment.
  • Теперь у нас есть две текстуры: 1×1 текстура “множитель” для HDR и текстура 2×2 гистограмма разложенная в 3 buckets. Оцениваем какая из часть гистограммы “перекошена” и сдвигаем значение в текстуре в нужную нам сторону - обычный рендер в текстуру, без зачитывания данных в CPU.
  • Результат: в зависимости от яркости кадра мы сдвинули Exposure Adjustment в нужную нам сторону, очень быстро, полностью на GPU и используя только текстуры 8 бит на канал.

Так выглядит текстура с коэфицентами яркости
=================================================

Вот тут, можно скачать демо со всеми исходниками (37.7Мб) - в котором HDR реализован описанным выше способом. Демо работает на PC и на X360 (у кого есть XDK + Devkit могут тоже потестировать). Там совсем простой алгоритм экспо-коррекции (обычный сдвиг), можно подумать и сделать похитрее. Также, развивая идею дальше: коэфицент Exposure Adjustment не обязательно должен быть константным для всего кадра. Мне кажется, если сделать для различных зон экрана разную коррекцию экспозиции (например сеткой 16×16) то будет еще лучше.

Управление: зажав правую кнопку мыши можно крутить головой и летать с помощью WASD.

Заодно, добавил ToneMaping как в Uncharted2,  хотя он и не обязателен, можно просто клемпить получившийся результат.

update: обновил демку, на экран для отладки выводятся результаты анализа картинки и искуственно добавил тормозов для CPU, иначе при FPS > 1000 float дельта тайм себя плохо ведет :)


Данный пост размещен в моем личном блоге SergeyMakeev.com.
Tags: игрострой
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 2 comments