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

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

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


4 комментария:

Sergey Miryanov комментирует...

Есть 2 решения.
1) При рисовании маски закрашивать пиксели на границе в черный цвет.
2) Использовать clip planes. 4 штуки по сторонам бокста материала.


Какая разница между решениями?

Timai комментирует...

Какая разница между решениями?

При 1-ом варианте нужно не забывать закрашивать границы в чёрный цвет. Если генерация масок идёт инструментарием - никаких проблем. Если маски же рисуются/корректируются в фотошопе - артистам нужно не забывать об этой фиче

При 2-ом подоходе теоритически должен чуть проседать фпс. Но не замерял, так что могу ошибаться( может даже будет быстрее, ибо не происходит растеризация "ненужных" пикселей ).

.: Quest alliance :. комментирует...

хм.. а затраты на DIP не жалко?

Timai комментирует...

Нет, учитывая что в боевых условиях можно рисовать по 2-3 маски за патч (и один dip). Этого должно хватить на бОльшую часть террейна.