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

понедельник, 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-х масок, рисуем в несколько проходов.

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

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


суббота, 29 декабря 2007 г.

Про нормали

Лод на то и лод, чтобы быть простым и быстрым. Но ведь хочеться чтобы, вдобавок, было и красиво.

Что мы имеем изначально:



Освещение не очень, правда? Как можно это исправить? Правильно, картой нормалей. Откуда её взять? Ну уж не рисовать самому конечно =) У нас же есть высокодетализированная геометрия ландшафа( патчи ).

Выствляем матрицы:

view = math::matrix_look_at_lh(
view,
vec3( 0.f, land_bbox.max.y, 0.f ),
vec3( 0.f, 0.f, 0.f ),
vec3( 0.f, 0.f, 1.f ) );

proj = math::matrix_ortho_off_center_lh(
proj,
0.f,
land_bbox.width(),
0.f,
land_bbox.depth(),
0.f,
land_bbox.height() );
_Winnie C++ Colorizer

В шейдере не стоит забывать, что просто взять и записать в текстуру нормаль нельзя. Ибо она может содержать отрицательные значения. Посему пишем так:
float4 ps_main( in float3 norm : TEXCOORD0 ) : COLOR
{
normal = ( normal + 1.f ) / 2.f;
return float4( norm.x, norm.z, norm.y, 1.f );
}
_Winnie C++ Colorizer

Заодно я поменял y и z координаты, чтобы в дальнейшем не было путанницы ( привет, нелюбимая система координат 3dmax ).

В результате иммем:



Осветить всё это дело проблем уже не составляет.



Таким образом получаем вполне прилично выглядящий лод, который не будет резко выделятся на фоне более детализированнх патчей.

пятница, 28 декабря 2007 г.

Пару слов про ландшафт

Ландшафт будем строить по уже сложившейся схеме. Лод + патчи.
Лод для ландшафта генериться на этапе загрузки. Делается это очень просто, поэтому описание я опущу.
Размер лода и патчей берутся из настроек карты. Чтобы твикать было удобнее.



Рисум так - сначала лод, после - патчи вокруг камеры. Радиус видимости патчей так же берём из настроек.
Маленькая хитрость - очищать Zbuffer после отрисовки лода. Ибо могут вылезти артефакты. Так же иногда лод нужно немного опустить вниз по Y. Тоже для борьбы с артефактами. Тоже берётся из настроек карты.

Разница между сеткой лода и патчами:



Более подробно про отрисовку пачтей чуть позже.

О карте высот и пользе размытия

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

Но иногда вылазят мелкие пакости, которые портят жизнь.

Рассмотрим вариант, где количество вершин соответсвует количеству пикселей в карте высот. А высота вершины считается как lerp( min_height, max_height, pixel / 255 )

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



Появляется некая "ступенчатость". Почему? Дело в том что при использованнии карты высот с разрешением 8 бит на пиксель, мы имеем рабочий диапазон значений 0-255. При перепаде высот, скажем, в 100 метров, минимальный шаг между высотами оказывается: 100 / 256 ~ 0.39м.
Именно это низкое разрешение и является причиной "ступенчатости"



Как это побороть? Самый простой способ - увеличить рабочий диапазон. К примеру, до 16 бит, вместо 8ми. Тогда диапазон значений 0-65535. Что даст нам шаг в 100 / 65536 ~ 0.001м. Неплохо, да?

Но это повлечёт за собой увелечение размера карты высот в 2 раза. Что, зачастую, нежелательно.

Есть ещё один путь, требующий небольшого количества телодвижений. Размытие. Или, если будет угодно "усреднение значений".
Блурим в 2 прохода( горизонтальный + вертикальный ) результирующие высоты с небольшим радиусом и получаем приятную картинку =)



Артефакты пропали. Разница очевидна.



Такие дела.