вторник, 27 мая 2008 г.

Volumetric fog

Недавно, когда я проходил собеседование в одну компанию, был задан вопрос о тумане - типы, примеры реализации и т.д. И вот тогда я вспомнил, что давно хотел пощупать 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;

void vs_depth( inout float4 pos : POSITION ,
out float depth : TEXCOORD0 )
{
pos = mul( pos, g_wvp );
depth = pos.z;
}

float4 ps_depth( in float depth : TEXCOORD0 ) : COLOR
{
return float4( depth, 0.f, 0.f, 0.f );
}
_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-х полигонов со специальным шейдером. Как при постпроцессинге, да.

Степень "затуманенности" каждого пикселя может посчитать по формуле:

k = ( fog_back - fog_front ) * fog_density
где
  fog_back - расстояние от камеры до "задней" части тумана
  fog_front - расстояние от камеры до "передней" части тумана
  fog_density - коэффициент плотности. Через расстояние dist * fog_density туман полностью скроет объект.

Всё хорошо, но полученный туман игнорирует объекты сцены. Чтобы исправить это, нужно снять глубину самой сцены (она так же нужна для таких вещей для soft particles или depth of field) и немного изменить нашу формулу.

k = ( ( fog_back < scene_depth ? fog_back : scene_depth ) - fog_front ) * fog_density

где
  scene_depth - глубина сцены в данной точке

Если Вы не любите IF'ы в шейдере, то можно написать так:

k = ( ( fog_back - fog_front ) - ( fog_back - clamp( scene_depth, 0, fog_back ) ) * fog_density


Alpha blending
Пару слов о том, как накладывать полученное изображение на сцену.
В случае когда мы имеем дело с белым туманом - всё просто, достаточно выставить аддитивный блендинг и выводить цвет как

return float4( fog_color * k, 1.f )
_Winnie C++ Colorizer


Но в случае если мы захотим использовать цвет отличный от белого, то появятся артефакты. Почему? Аддитивный блендинг считается по формуле:

res_color = src_color + dest_color

Предположим что у нас зелёный туман закрывает красный объект. Что мы получим?

res_color = ( 0.f, 1.f, 0.f ) + ( 1.f, 0.f, 0.f ) = ( 1.f, 1.f, 0 )

Внезапно! Желтый цвет.

Как этого избежать? Выставляем 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
{
float fog_back = tex2D( s_fog_back , tex ).r;
float fog_front = tex2D( s_fog_front , tex ).r;
float scene_depth = tex2D( s_scene_depth , tex ).r;

float k = fog_back - fog_front;
k -= fog_back - clamp( scene_depth, 0, fog_back );

return float4( g_fog_color, k * g_fog_density );
}
_Winnie C++ Colorizer


Что мы получили в итоге:


Да, кстати, камеру можно помещать внутрь тумана, рисоваться будет по прежнему правильно:


Итого
Техника получилась довольно простая (на написании статьи ушло больше времени) и лёгкая в реализации. К тому же, её не сложно прикрутить к уже готовому движку, ибо не нужно вносить существенных изменений в render pipeline. Все действия можно производить после окончательного рендера сцены (но до постпроцессинга).

Где это можно использовать? На вскидку:
1) Непосредственно сам объёмный туман
2) Пар стелящийся по земле. В этом случае можно оптимизировать алгоритм и убрать 1 проход
3) Добавить вершинную анимацию, чтобы туман "заиграл"
4) Вода. Мутная вода. Жидкости в больших сосудах
5) Volumetric light
6) Игровые эффекты. Можно сделать симпатичных "призраков"
7) Добавить анимированную текстуру для выборки альфы - пар или дым
8) Мелькает мысль про использования схожего алгоритма для теней. Должно получиться любопытно, ибо можно сделать очень "мягкие" тени

Ну и ещё в нагрузку пару картинок