вторник, 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) Мелькает мысль про использования схожего алгоритма для теней. Должно получиться любопытно, ибо можно сделать очень "мягкие" тени

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

понедельник, 28 января 2008 г.

Растительность. Часть 3( заключительная ). Ветер

Ветер
Пришло время оживить нашу растительность. Для этого придётся добавить немного ветра.
Физика стеблей очень похожа на физику резиновой палки ( звучит тупо, знаю ). Если к нему прикладывать силу, он отклониться, но стоит силе исчезнуть, стебель вернётся к своей первоначальной форме.

Для начала нужно определить несколько новых коэффициентов.

max_deflection - максимальное расстояние, на которое может отклониться куст.
flex - На сколько отклонится стебель при ветре 1м/с.
recovery_speed - С какой скоростью модель будет возвращаться к своей первоначальной форме.

Высчитываем смещение стеблей.

static vec2 current_offset( 0.f, 0.f );

float current_offset_lenght = math::vec_lenght( current_offset );
//
vec2 recovery_dir = math::vec_normalize( recovery_dir, -current_offset );
//
float deflection_k = current_offset_lenght / max_deflection;
float wind_power = wind_speed * flex * time_step;
float recovery_power = recovery_speed * deflection_k * time_step;
//
current_offset += wind_dir * wind_power + recovery_dir * recovery_power;

math::clamp( current_offset.x, current_offset.x, -max_deflection, +max_deflection );
math::clamp( current_offset.y, current_offset.y, -max_deflection, +max_deflection );
_Winnie C++ Colorizer


Как видно из вышеуказанного кода, на куст действует одновременно 2 силы - ветер и восстановление. Когда ветер стихает, модель возвращается к своей первоначальной форме.

Кстати, ветер у нас 2-хмерный( т.к. вверх/вниз трава не деформируется ). Поэтому считаем смещение только по X и Z осям.

Трава у нас стала "подчинятся" ветру, но этого мало. В реальном мире она "колышется". Этот эффект( в основном ) достигается за счёт того что ветер никогда не бывает статичным. Он постоянно меняет своё направление и силу в небольшом диапазоне.

Реализовать этот эффект можно, к примеру, так:
const float wind_minmax_k = 0.5f;
float wind_max_power = wind_power;
float wind_min_power = wind_power * wind_minmax_k;
//
static float wind_amplitude = 0.f;
static float wind_amplitude_dir = 1.f;
const float wind_amplitude_speed = 0.5f;
//
float rand_k = math::lerp( 0.25f, 1.f, ( float )rand() / ( float )RAND_MAX );
//
wind_amplitude += wind_amplitude_speed * wind_amplitude_dir * m_time_step * rand_k;

if( wind_amplitude > 1.f || wind_amplitude < 0.f )
wind_amplitude_dir = -wind_amplitude_dir;
//
math::saturate( wind_amplitude, wind_amplitude );

float wind_speed = math::lerp( wind_min_power, wind_max_power, wind_amplitude );
_Winnie C++ Colorizer


Данный код позволяет эмулировать порывы, линейно меняя скорость ветра между двумя значениями. А внесение в расчёты небольшого количества хаоса ( rand_k ) добавит немного естественности.

Трансформация
Мы рассчитали на какое расстояние должен сместиться стебель ( current_offset ), осталось его нарисовать.

Очевидно, что смещать мы должны верхние вершины стеблей. Определить где верх, где низ, очень просто - текстурные координаты у нас идут сверху вниз. Зная это, легко определить высоту вершины ( 1.f - tex_coord.y ).

Может возникнуть соблазн сдвинуть только XZ координаты, как показано тут:
1,64 КБ


Но это неправильно. Возмите ручку, поставьте её вертикально на стол и попробуйте наклонить. Заметьте, что она движется по окружности:
4,11 КБ


Нам нужно поступить аналогично. Делать "честное" смещение по окружности несколько "дороговато", поэтому обойдёмся упрощённой моделью. Уменьшим высоту на ту же величину, на сколько сдвинули её ветром.
1,61 КБ


Вот что у нас получилось в итоге


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

Пора двигаться дальше.

p.s. Если честно, я не очень доволен получившимся ветром. Был потрачен не один день, и всё равно, вышло немного не то что хотелось. Но слишком затягивать тоже не дело, ибо пресловутый "синдром улучшения" может растянуть процесс на неопределённый срок. Может ещё вернусь к этой теме.

четверг, 17 января 2008 г.

Растительность. Часть 2_1( отступление). В поисках идеальной фигуры

Оригами из полигонов
В качестве модели пучка травы я использовал т.н. "звёздочку" - 4 плоскости, повёрнутые вокруг оси Y. Но у это фигуры есть очень серъёзный недостаток. Если направить камеру строго вниз - будет видна вся плоская сущность "кустика", что здорово аноит.

Пример:


Я экспериментировал с разными формами, пока не нашёл приемлемую. Перебрать пришлось довольно много.

Вот немногое из того что было опробовано( это примерно 1/5 часть из общего количества ):
27,53 КБ

В результате выбор был остановлен на этой фигуре:
22,80 КБ

Моделирование той же ситуации( камера направлена вниз ) с новой геометрией


Как видно, растительность приобрела ещё немного "объемности", а досадный артефакт исчез.

Новая геометрия добавила немного "природного хаоса", что положительно сказалось на общей картине

понедельник, 14 января 2008 г.

Растительность. Часть 2. Отрисовка

46,91 КБ

После того как мы сгенерировали матрицы [link] пришло время заняться непосредственно рендером.

Рисовать мы будет пучками. Геометрия - знакомая многим "звёздочка". 4 плоскости или 8 полигонов.


Она же после альфа теста:


Увеличим плотность засейки. Вблизи смотриться неплохо:


Но если отодвинуть камеру то вылезут артефакты.

Артефакт 1. wrap


На самой верхушки пучков повылазили артефакты. Их природа довольно проста и лечится всё это дело выставлением clamp'а, вместо wrap в самплере.

Артефакт 2. mipmaps и алиасинг
Если отодвинуть камеру ещё дальше, то можно заметить что картинка поменялась в худшую сторону.


Что произошло? Трава стала менее прозрачной. Почему? Кто работал с альфатестом - знают в чём проблема.
Всё дело в mipmap уровнях( если кто не знает что это - [link] )

Рассмотрим мип-уровни куста( альфа канал ).


Как видно, уже на 5-ом уровне альфа канал теряет все детали и превращается в пятно. В результате дальние кусты травы( использующие 4+ мип уровни ) теряют прозрачность и становятся более "монолитными".

Что первое приходит на ум - отключить сам mipmaping, в результате чего будет использоваться текстура с максимальным разрешением. Но сделав это, мы получим очень неприятный алиасинг.

( на превьюшке плохо видно, лучше смотреть полноразмерный скрин )

Очевидно, что отказаться от мип-уровней мы не можем. Что делать? Можно исхитриться и разбить текстуру 2 части. Первая - диффузная, вторая - альфа маска. Поместив их в разные самплеры и отключив мип уровни только для альфы мы можем побороть алиасинг.

Но есть способ проще - достаточно грузить только первые 3-4 мип-уровня. Для загрузки текстур я использую функцию D3DXCreateTextureFromFileEx, которая позволяет задавать количество загружаемых мип-уровней.

Как видно, алиасинг исчез, проблемы с альфа тестом тоже.


Instancing
Очевидно что рисовать один куст за вызов слишком "дорого". К примеру, у меня в кадре может быть более 4000 кустов. Делать 4000 вызовов на отрисовку( dip ) будет слишком накладно. Посему будем использовать shader constant geometry instancing.

Как оно работает. Известно, что vertex shader 2_0 и выше может работать как минимум с 256 регистрами( каждый регистр - это 4 float ), в которые можно затолкать 64 матрицы 4х4. Оставим 16 регистров для других нужд( видовая матрица, освещение и т.д ) и "забёрём" остальные. Итого 60 матриц в нашем распоряжении.

Чтобы использовать instancing нужна "особая" геометрия. Если кратко - батчинг с индексом на каждый элемент. Если подробно - каждый куст мы описывает 16-ю вершинами и 24-мя индексами. Нам нужно увеличить вершинный и индексный буффер в 60 раз и заполнить их копиями оригинала. Примерно так:

for( unsigned int j = 1 ; j < 60 ; j++ ) {
for( unsigned int i = 0 ; i < 16 ; i++ ) {
vertex[ j * 16 + i ] = vertex[ i ];
}
}
_Winnie C++ Colorizer


С индексами нужно поступить аналогично. В дополнение к сделанному нужно расширить формат вершины. Если раньше она содержала только позицию и текстурные координаты, то теперь она дополнилась индексом батча. Т.е. каждая вершина в "пучке" знает к какому индексу она относится.

for( unsigned int j = 0 ; j < 60 ; j++ ) {
for( unsigned int i = 0 ; i < 16 ; i++ ) {
vertex[ j * 16 + i ].index = j;
}
}
_Winnie C++ Colorizer


Буфера "забатчены" и проиндексированы. Осталось отрисовать.
float4x4 g_vp          : register( c0  );
float4x4 g_world[ 60 ] : register( c16 );

void vs_main( inout float4 pos : POSITION ,
inout float2 tex : TEXCOORD0 ,
in float index : TEXCOORD1 )
{
float4 world_pos = mul( pos, g_world[ index ] );
pos = mul( world_pos, g_vp );
}
_Winnie C++ Colorizer


В итоге количество dip calls сократилось в 60 раз. 100 вызовов вместо 6000. Думаю, разница очевидна. FPS радостно подпрыгнул, что и требовалось.

BTW: в DirectX 9 SDK есть очень хороший пример с instancing.

Итого
Комбинируя разные типы растительность можно получить очень приятную картинку




По моему, вышло неплохо =)

Растительность. Часть 1. Генерация.



Первоначально планировался один общий текст. Но т.к. получилось "слишком много букв" разбил на 3 части

Посмотрел я на ландшафт, что у меня получатся, и решил что чего-то не хватает. Было принято решение добавить немного растительности.

Генерация
Этап генрация довольно интересен. Ибо просто нагенерить случайные значения нельзя. Объясню почему.

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


Если сгенерировать рандомайзом позиции, то могут получиться весьма любопытные результаты.

Например вот


или вот


Очевидно, что такая "генерация" неприемлима, ибо абсолютно непредсказуема. Нужен алгоритм, при котором "засеивание" должно быть относительно равномерным. Как это сделать?

Для начала надо ввести коэффициент, описывающий плотность засеивания травы. Обозначим его как d( от density ).

d - количество объектов, расположенных на 1кв.м. поверхности

Очевидно что увеличивая d, увеличивается плотность засеивания. Получаем довольно "прозрачное" управление.

Нужно ввести ещё одно понятие - регион. Это область, содержащая 1 объект( куст ). С его помощью можно будет добиться равномерного засеивания.

u = 1 / ( sqrt( d ) )

u - Сторона региона.

Получаем:


Осталось только добавить немного "хаоса" =) Рандомно генерируем точку в пределах каждого региона.

Получаем:


Как раз то что и было нужно.

Рассаживаем на ландшафте
Область мы сгенерировали. Но этого мало, нужно ещё "рассадить" всё это дело на ландшафте. Тут, на самом деле, всё просто. Всё что нужно - это определить высоту( "y" координату ) точки ландшафта, где будет располагаться куст. Интереса ради я попробовал решить это проблему "в лоб". "Пробивал" лучом каждый треугольник патчей при помощи функции D3DXIntersectTri. Получилось ну очень медленно. Код был спешно стёрт и предан забвению.

Есть более оптимальный вариант. Ландшафт у нас строиться из карты высот? Да. Значит сетка регулярная? Да. Значит зная x и z координату( то что мы сгенерировали ) можно высчитать смещение и точно определить нужный треугольник? Да. Ну а 2 треугольника( квад ) проверить на пересечение побыстрее будет, чем несколько тысяч.

Если интересно как сделано у меня - посмотреть можно здесь [link] или здесь [link].

Вот что у нас получилось в итоге:


Маска
Маска нужна - это очевидно. Ибо квадратная область засеяной травы смотриться несколько... странно. Я использовал TGA формат( 8 бит на пиксель, без компрессии ) для этих целей. Какой точке на маске соответствует "травинка" считаем:

mask_offset_x =
( ( grass_pos.x - grass_region_bbox.min.x ) /
grass_region_bbox_width ) * mask_width );
mask_offset_y =
( ( grass_pos.z - grass_region_bbox.min.z ) /
grass_region_bbox_height ) * mask_height );
_Winnie C++ Colorizer


Дальше я считал так - если цвет пикселя менее 64, то сажать в этом месте мы ничего не будем. Если же значение > 64, то цвет будет использовать для масштабирования. Считаем как

scale_factor = ( mask_pixel - 64 ) / ( 255 - 64 );

Итого
После всех этих сложных телодвижения можно наконец-то построить матрицу каждой травинки. Для "естественности" добавим немного рандомайза в финальные вычисления. Итого мы получаем:
float scale = math::lerp(
scale_min, scale_max, ( float )rand() / ( float )RAND_MAX );
scale *= scale_factor;

matrix mat_tr = math::matrix_translation(
mat_tr, pos );
matrix mat_sc = math::matrix_scaling(
mat_sc, vec3( 1.f, 1.f, 1.f ) * scale );
matrix mat_rot = math::matrix_rotation_y(
mat_rot, ( float )rand() / ( float )RAND_MAX );

matrix mat_res = mat_sc * mat_rot * mat_tr;
_Winnie C++ Colorizer


Всё, генерация закончена. Следующий этап - отрисовка( будет завтра ).

понедельник, 7 января 2008 г.

О масках

30,87 КБ
Визуализация открытых пространств - штука весьма интересная. Сгенерировать/загрузить саму геометрию ландашфта - это не так сложно. А вот грамотно и красиво его "разукрасить" - это уже поинтереснее.

В настоящий момент большая часть игр, действие которых происходит на открытых пространствах используют маски + затайленные текстуры. Если кто не совсем понял о чем речь - поясню. Нарисовать и использовать одну текстуру для всего ланшафта - слишком "дорого" для железа. Ибо при обычном подходе( есть ещё "необычный", об этом ниже ) такая текстурка просто отжрёт всю видеопамять( если вообще сможет загрузиться ). Поэтому используют маски. Маска - это обыная ч/б текстура. Диффузный цвет пикселя считается как ( diffuse_texture_0 * mask_0 + diffuse_texture_1 * mask_1 ... и т.д. ). Думаю, мысль ясна.

Существуют две основные техники рисования масками( их на самом деле больше, я остановлюсь на самых популярных )
1) Маска в геометрии ландшафта. Как сделано в Oblivion. Каждая вершина содержит 4-х байтный цвет, который раскладывается на 4 маски. Основной минус - увеличить детализацию можно только увеличив плотность сетки.
2) Маски в текстуре. Одна на всю локацию. Как сделано в Neverwinter Nights 2. На самом деле, в одной текстуре 4 маски, просто они лежат в разных RGB каналах. Как видно - предел 4 маски ( 4-ая счиатется как 1.0 - R - G - B ). Основной минус - невозможно увеличить детализацию какой-либо области. Только всю текстуру целиком.

Есть ещё относительно новый подход - "Мегатекстура" от дядюшки Кармака. Вот тут очень популярно о ней написно [link]. Техника довольно проста в реализации. Основной труд - написание инструментария для хранения и генерации этой "мегатекстуры". Я ещё коснусь этой темы в конце поста.

В своё время я реализовывал 1-ый вариант. Причём очень хитрый и заковыристый. С разбивкой на субматериалы, с неограниченным количеством масок и т.д. Но главной проблемы избежать так и не удалось - без увеличения плотности сетки нельзя было увеличить делализацию маски. К примеру - есть ландшафт. На нём хочеться нарисовать тропинку. Нарисовать-то мы её нарисуем, но в итоге увидим что края этой дорожки сильно размыты. Почему? Предположим, шаг сетки у нас 1 метр. В одной веришине значение маски 1.0 в другой 0.0. Что случиться? Правильно, инторполяция. В итоге имеем метровый градиент, дающий малопривлекательную "размытость". Регионы травы, песка и камней рисовать пойдёт, но не более.

Нужен другой подход, сочетающий в себе произвольную точность и низкую ресурсоёмкость. Как этого можно добиться? Нужно "освободить" маски. Чтобы они не были привязаны ни к геометрии, ни размеру текстуры. Как? Разбивать на произвольные регионы. Сейчас объясню как.

Определям регион маски. Вот он - красный aabbox в центре.


Теперь нам нужно в этом месте нарисовать маску. Что для этого нужно сделать? Правильно, пересчитать текстурные координаты.

float2 world_size = float2( world_max.x - world_min.x, world_max.y - world_min.y );
float2 local_size = float2( local_max.x - local_min.x, local_max.y - local_min.y );
tex.x -= local_min.x / world_size.x;
tex.x *= world_size.x / local_size.x;
tex.y -= ( world_size.y - local_max.y ) / world_size.y;
tex.y *= world_size.y / local_size.y;
_Winnie C++ Colorizer
где
world_min, world_max - aabbox ландшафта
local_min, local_max - aabbox области

Чтобы не считать всё это в шейдере, посчитаем "снаружи" и передадим в качестве матрицы.
const vec2 world_size( world_bbox.width(), world_bbox.depth() );
const vec2 local_size( local_bbox.width(), local_bbox.depth() );

tex_mat_tr = math::matrix_translation(
tex_mat_tr,
-vec3(
local_bbox.min.x / world_size.x,
( world_size.y - local_bbox.max.z ) / world_size.y,
0.f ) );

tex_mat_sc = math::matrix_scaling(
tex_mat_sc,
vec3( world_size.x / local_size.x, world_size.y / local_size.y, 0.f ) );

tex_mat = tex_mat_tr * tex_mat_sc;
_Winnie C++ Colorizer


Кидаем tex_mat в вершинный шейдер, а там считаем новые текстурыне координаты как

tex = mul( float4( tex.x, tex.y, 1.f, 1.f ), g_tex_mat ).xy;
_Winnie C++ Colorizer

И вот, мы получили маску в нужном нам месте.


Важное замечание: нужно обязательното включить "clamp" для текстуры маски. Иначе она пойдёт тайлиться на соседние области, что недопустимо. Тайлиться он перестал, но этого мало, нужно чтобы она ( маска ) не затронула соседние регионы. Есть 2 решения.
1) При рисовании маски закрашивать пиксели на границе в черный цвет.
2) Использовать clip planes. 4 штуки по сторонам бокса материала.

Маска отображается корректно? Самое время добавить диффузную текстуру.
float mask = tex2D( s_mask, tex_default ).r;
float3 diffuse = tex2D( s_diffuse, tex_scale );
return float4( diffuse, mask );
_Winnie C++ Colorizer

Получаем:


Как нетрудно заметить, маску мы ложим в альфу. Посему предварительно нужно включить alphablending( src_blend - blend_src_alpha, dest_blend - blend_inv_src_alpha ).

При желании диффузную текстуру можно затайлить, домножив трансформированные текстурные координаты на scale factor.


И всё =)
В итоге, варьируя регионы маски и её разрешение можно получить абсолютно любую точность.


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


Про оптимизацию
Ландшафт у меня разбит на патчи. В среденем на 1 патч приходиться 2-3 материала. Рисовать каждую маску за отдельный проход - конечно же, неоптимально. Но лечиться это очень просто. Можно( и нужно ) рисовать несколько масок за раз. Подход такой же, как и при отрисовки одного объекта сразу с несколькоми источниками света. Создаётся 3 шейдера на каждый тип материала( отрисовка одной маски, 2-х и 3-х ). На этапе загрузки определяем сколько материалов на патче, биндим соответсвующий шейдер и всё. Если на патче более 3-х масок, рисуем в несколько проходов.

Про мегатекстуру
Данный подход можно использовать совместно с "мегатекстурой". Для её генерации конечно же, а не отрисовки =) Мысль довольно интересная, может я её разовью в дальнейшем.

Итого
Получилась очень хорошая техника. По крайней мере, мне нравится как она работает.


среда, 2 января 2008 г.

О ссылающихся друг на друга объектах

Очень часто возникает ситуация, при которой объект, объявленный раньше, должен вызывать метод объекта, объявленного позже. Но поменять объявления местами не представляется возможным, ибо "нижестоящий" по коду ссылается на "вышестоящего".

Пример:

class foo
{
public:
void test( bar * b ) { b->print( "foo" ); }
};

class bar
{
public:
void print( const char * txt ) { printf_s( "%s\n", txt ); }
private:
foo m_obj;
};
_Winnie C++ Colorizer

При попытки скомпилировать это, компилятор пошлёт к такой-то матери. Ну и понятно, ведь на момент компиляции foo, нет никакой информации о bar. Это же не позволяет использовать обычные указатели на методы класса.

Можно попытаться выкрутиться, используя интерфейсы:

struct i_bar
{
virtual void print( const char * txt ) = 0;
};

class foo
{
public:
void test( i_bar * b ) { b->print( "foo" ); }
};

class bar : public i_bar
{
public:
void print( const char * txt ) { printf_s( "%s\n", txt ); }
private:
foo m_obj;
};
_Winnie C++ Colorizer

Данный код скомпилиться и будет работать. Но этот подход требует создание "костылей", что не радует глаз. К тому же нарушается иерархия. foo приходиться знать о bar, хотя знать о нём он не обязан. Всё что нужно - это вызов некой "абстрактной" функции/метода. А чья она - это уже не важно.

Можно "разнести" объявление и реализацию классов:
// bar.h
#include "foo.h"

class bar
{
public:
void print( const char * txt ) { printf_s( "%s\n", txt ); }
private:
foo m_obj;
};

// foo.h
class bar;
class foo
{
public:
void test( bar * b );
};

// foo.cpp
#include "foo.h"
#include "bar.h"

void foo::test( bar * b )
{
b->print( "foo" );
}
_Winnie C++ Colorizer

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

Пока не начала курить boost =)
typedef boost::function< void( const char * ) > boost_func;

class foo
{
public:
void test( boost_func & func ) { func( "foo" ); }
};

class bar
{
public:
void print( const char * txt ) { printf_s( "%s\n", txt ); }
private:
foo m_obj;
};


int _tmain( int, char * )
{
foo f;
bar b;

f.test( boost_func( boost::bind( &bar::print, &b, _1 ) ) );

return 0;
}
_Winnie C++ Colorizer

Вуаля! boost::bind + boost::function решает все проблемы. Раскидывать ничего не надо. К томуже foo не приходиться ничего знает о bar. Одни плюсы =)

Вот только я ещё не до конца понял, как это штука работает. Но работает - факт.