Показаны сообщения с ярлыком vegetation. Показать все сообщения
Показаны сообщения с ярлыком vegetation. Показать все сообщения

понедельник, 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


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