Недавно, когда я проходил собеседование в одну компанию, был задан вопрос о тумане - типы, примеры реализации и т.д. И вот тогда я вспомнил, что давно хотел пощупать volumetric fog, но всё как-то руки не доходили. Время свободное есть, почему бы не попробывать?
Volumetric fog
Для начала - что же такое volumetric fog (объёмный туман) и для чего он нужен? У обычного тумана есть один недостаток - он окутывает всю сцену. Этого обычно достаточно чтобы воссоздать вид "утреннего тумана", но если мы хотим сделать нечто посложнее, например, туман, стелющийся по земле, то эта техника не подходит.
Нужно придать ему форму. Как? Сейчас это выясним.
Чуточку теории
Туман — форма выделения паров воды в виде микроскопических капель или ледяных кристаллов, которые, собираясь в приземном слое атмосферы, делают воздух менее прозрачным [link].
Если опустить детали, то туман - это частицы, частично препятствующие видимости. И, соответственно, чем больше этих частиц между наблюдателем и объектом, тем хуже этот объект видно. В компьютерной графике обычно используют 2 формулы для определения степени видимости в точке - линейную и экспоненциальную.
Я в своём примере буду использовать линейный тип.
Снятие глубины. Depth texture
Для реализации задуманного нам нужно будет снимать глубину сцены. Вкратце опишу что это такое (если вдруг кто забыл).
Очень часто для реализации определенных техник может понадобиться доступ к depth buffer сцены, но в DX9 это не так просто. Что делать? На помощь приходит специальный текстурный формат - D3DFMT_R32F, который позволяет писать 32-х битный float в текстуру.
1) Создаём текстуру данного формата с размерами, идентичными размеру окна (разумеется один раз, а не на каждый кадр)
2) Выставляем её в качестве render target
3) Устанавливаем специальный шейдер
4) Рисуем сцену
5) Возвращаем оригинальный render target
Рекомендую так же создать отдельный depth stencil surface для всех render to texture проходов, ибо это позволит избежать многих проблем (в т.ч. с multisampling).
Шейдер может выглядеть так:
float4x4 g_wvp; |
_Winnie C++ Colorizer |
Хочу обратить внимание на строчку
depth = pos.z |
_Winnie C++ Colorizer |
Это т.н. "быстрый" расчёт глубины. Для повышения точности рекомендуют использовать формулу:
depth = distance( world_pos, camera_eye_pos ) |
_Winnie C++ Colorizer |
где
world_pos - вершина, трансформированная по мировой матрицы
camera_eye_pos - положение наблюдателя.
Но для нашего случая высокая точность не обязательна, поэтому будем использовать "быстрый" вариант.
После этого прохода мы получим текстуру, в red-компоненте которой будет лежать глубина данного пикселя.
Hint: во время съёма depth-текстуры существует одна тонкость. Если мы делаем IDirect3DDevice9::Clear и в качестве параметра цвета задаём значением 0x00ff0000 (красный цвет), то в финальной текстуре мы и получим в качестве глубины 1.f.
Т.е. к примеру на сцене у нас один единственный объект - чайник. На depth-текстуре на месте этого чайника глубина будет правильная, в вот вся остальная область будет забита единицами. Как будто на расстоянии 1.f от камеры мы нарисовали плоскость перекрывающую весь экран.
Такая проблема редкость, ибо в игровых проектах сцена занимает всю область экрана (даже D3DCLEAR_TARGET флаг выставлять не нужно). Но если всё же надо грамотно очистить - рисуйте большой (больше сцены) куб во время снятия глубины в текстуру, с D3DCULL_CW (по часовой). Это заполнит текстуру правильными значениями.
Создаём туман
В качестве формы тумана можно использовать любую геометрию. Единственное условие - она должна быть замкнутой. Почему - будет понятно по ходу объяснений.
Как действует туман? Степень видимости объекта зависит от того, через какой количество частиц тумана прошёл луч света от объекта до глаза. Соответственно если мы можем определить "количество" тумана, лежащего между камерой и объектом, мы можем расчитать степень видимости в данной точке. Как это сделать?
1) Для начала нам нужно создать depth-текстуру с "задними" полигонами геометрии тумана. Как это делается, я описал выше. Есть 3 "но":
a) Очищаем текстуру (IDirect3DDevice9::Clear) с параметром Z = 0.f (а не Z = 1.f как обычно)
b) Меняем render state D3DRS_ZFUNC на D3DCMP_GREATER (вместо D3DCMP_LESSEQUAL), это заставит работать depth-buffer "наоборот"
c) Выставляем D3DRS_CULLMODE в D3DCULL_CW( вместо D3DCULL_CCW )
2) Cоздаём depth-текстуру для "передних" полигонов тумана. Тут всё как обычно.
В результате мы имеем 2 текстуры с "началом" и "концом" тумана в каждой точке. Зная это, можно без проблем найти "плотность" тумана для каждого пикселя сцены.
Туманим
Рисовать будем так - на уже отрендеренную сцену накладываем прямоугольник из 2-х полигонов со специальным шейдером. Как при постпроцессинге, да.
Степень "затуманенности" каждого пикселя может посчитать по формуле:
fog_back - расстояние от камеры до "задней" части тумана
fog_front - расстояние от камеры до "передней" части тумана
fog_density - коэффициент плотности. Через расстояние dist * fog_density туман полностью скроет объект.
Всё хорошо, но полученный туман игнорирует объекты сцены. Чтобы исправить это, нужно снять глубину самой сцены (она так же нужна для таких вещей для soft particles или depth of field) и немного изменить нашу формулу.
где
scene_depth - глубина сцены в данной точке
Если Вы не любите IF'ы в шейдере, то можно написать так:
Alpha blending
Пару слов о том, как накладывать полученное изображение на сцену.
В случае когда мы имеем дело с белым туманом - всё просто, достаточно выставить аддитивный блендинг и выводить цвет как
return float4( fog_color * k, 1.f ) |
_Winnie C++ Colorizer |
Но в случае если мы захотим использовать цвет отличный от белого, то появятся артефакты. Почему? Аддитивный блендинг считается по формуле:
Предположим что у нас зелёный туман закрывает красный объект. Что мы получим?
Внезапно! Желтый цвет.
Как этого избежать? Выставляем src_blend = D3DBLEND_SRCALPHA, dest_blend = D3DBLEND_INVSRCALPHA и выводим цвет как
return float4( fog_color, k ) |
_Winnie C++ Colorizer |
Последний этап
Вся подготовка завершена, теперь можно рисовать полученные туманности.
Шейдер отрисовки тумана ( всё очень просто ):
float4 ps_draw_fog( in float2 tex : TEXCOORD0 ) : COLOR |
_Winnie C++ Colorizer |
Что мы получили в итоге:
Да, кстати, камеру можно помещать внутрь тумана, рисоваться будет по прежнему правильно:
Итого
Техника получилась довольно простая (на написании статьи ушло больше времени) и лёгкая в реализации. К тому же, её не сложно прикрутить к уже готовому движку, ибо не нужно вносить существенных изменений в render pipeline. Все действия можно производить после окончательного рендера сцены (но до постпроцессинга).
Где это можно использовать? На вскидку:
1) Непосредственно сам объёмный туман
2) Пар стелящийся по земле. В этом случае можно оптимизировать алгоритм и убрать 1 проход
3) Добавить вершинную анимацию, чтобы туман "заиграл"
4) Вода. Мутная вода. Жидкости в больших сосудах
5) Volumetric light
6) Игровые эффекты. Можно сделать симпатичных "призраков"
7) Добавить анимированную текстуру для выборки альфы - пар или дым
8) Мелькает мысль про использования схожего алгоритма для теней. Должно получиться любопытно, ибо можно сделать очень "мягкие" тени
Ну и ещё в нагрузку пару картинок