[Перевод] Система динамического разрушения стен в Radio Viscera

6dd7c7291414e7445f929c7fd6ad1a9b.jpg


В этой статье я расскажу о системе динамических разрушений, созданной для видеоигры Radio Viscera.

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

d2190b630927614cb12ae04faf12d53b.jpg


Рисунок 1. Базовые концепции, лежащие в основе системы
Система разрушений основана на трёх элементах, совместно создающих иллюзию пробивания отверстия в стене:

Параметрическая геометрия — используется для рендеринга повреждённых стен.

Физический движок — управляет формами коллизий, делающими стену твёрдой, выполняет запросы рейкастов и помогает генерировать триангулированные формы коллизий при повреждении стены.

Render textures — хранят данные повреждений для каждой стены, передаваемые в геометрический шейдер, а также используются как источник для генерации форм коллизий.

2. Изначальная генерация граней


Каждая разрушаемая грань состоит из трёх частей. Первая часть — это отображаемый меш, который является параметрически сгенерированной плоскостью с позицией, данными нормалей и UV-координат. Он используется для рендеринга грани в игре.

Вторая часть — это форма коллизий (collision shape). Это статичное твёрдое тело с учётом коллизий, добавляемое в динамический мир симуляции физики. Сама форма идеально соответствует отображаемому мешу и расположена прямо поверх него. Кроме того, для обеспечения коллизий твёрдая стена должна обнаруживать запросы рейкастов разрушений (см. раздел «Обнаружение рейкастов»).

70f422af9277daa6651f4ce5ce449b1f.jpg


Рисунок 2. Изначальные компоненты неповреждённой грани (отображаемый меш, collision shape, буфер повреждений)

Третья часть — это буфер повреждений. Это двухмерная render texture, используемая для хранения состояния повреждений грани. Размеры этой текстуры вычисляются таким образом, чтобы они соответствовали размеру грани в пространстве мира, умноженному на коэффициент «пиксели на метр», определяющий разрешение, с которым будут храниться повреждения. В своём случае я использовал коэффициент 48 пикселей на метр, поэтому стена размером 4×2 метра в пространстве мира будет иметь буфер повреждений размером 192×96 пикселей. Эта текстура очищается сбросом на RGBA [0,0,0,0].

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

3. Обнаружение рейкастов и применение разрушений


Разрушения применяются при помощи рейкастов. При выстреле из оружия выполняется тест рейкаста со всеми телами динамического мира, которые имеют коллизии. Луч испускается из ствола оружия и распространяется в сторону текущего направления прицела. Если рейкаст пересекается с collision shape повреждаемой грани, то регистрируется попадание. Фиксируются позиция в мире и направление нормалей в точке пересечения, а затем они передаются сущности повреждаемой грани, чтобы попадание можно было записать в буфер повреждений.

8e03d3bc91328296f81f2bbb1f26aea9.jpg


Рисунок 3. Нормаль к поверхности попадания используется для направленных эффектов (см. ниже)

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

a6c0997fbb1e15c61481843edf29eb4d.jpg


Рисунок 4. Этапы, необходимые для отрисовки повреждений в буфер

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

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

image-loader.svg


Рисунок 5. Спрайт повреждения в увеличении и с повышенной яркостью

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

4. Отрисовка повреждённой стены


92e1b3b7828ae4f52388a6515b7d0814.jpg


Рисунок 6. Каркас меша после тесселяции. Повреждения не применены.

Когда нетронутой стене впервые наносится урон, происходит несколько действий. Сначала отображаемый меш, который ранее был всего лишь плоскостью из двух треугольников, подвергается тесселяции. Как и при вычислении разрешения буфера повреждений существует коэффициент плотности мешей, задаваемый в «гранях на метр». Я использую коэффициент 6, поэтому стены 4×2 метра создают меш, состоящий из квадратных секций 24×12 (576 треугольников). Синий канал цвета вершин используется как булево значение, обозначающее вершины, находящиеся на самых внешних рёбрах плоскости меша. Это позволяет мне экструдировать эти вершины в геометрическом шейдере для создания толщины стены, которую мы видим вдоль верхнего ребра.

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

f28cc97c09dcb27822298626a523bf9b.jpg


Рисунок 7. Визуальное представление двух множеств UV (снизу) с текстурами, которые они сэмплируют (сверху)

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

Далее я буду описывать шейдер словами. Если вы предпочитаете читать псевдокод, то опуститесь немного ниже.

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

Если ни одна из вершин не повреждена, то треугольник должен отрисовываться обычным образом. Такие треугольники нужно отрисовывать дважды: один раз как обычно, второй — экструдированными внутрь с инвертированными нормалями и обратным порядком обхода. Благодаря этому стена выглядит двусторонней и имеющей толщину. Перед завершением шейдер также должен проверить, находятся ли какие-то из этих вершин по краям при помощи атрибута цвета вершин, который мы использовали ранее. Если это вершина края, то шейдер сгенерирует плоскую геометрию, закрывающую верхнее ребро и маскирующую пустоту между двумя гранями стены.

Последний случай — самый сложный: если одна или несколько вершин треугольника имеют значение повреждения выше 0, но ниже порога полного разрушения. Позиция повреждённой вершины преобразуется — чем больше повреждений получила вершина, тем сильнее она будет перемещена внутрь, как относительно стены, так и относительно соседних вершин. Такое преобразование применяется и к исходным вершинам, и к инвертированным вершинам «внутренней стены». В результате получается ребро полигона, уменьшающееся и приближающаяся с увеличением повреждений, благодаря чему геометрия стены становится всё тоньше, пока значение повреждений не достигнет пороговой величины и геометрия не будет полностью отброшена.

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

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

Псевдокод геометрического шейдера

// Vertex attribute forwarded from vertex stage
in vec4 aColor;

// Threshold is a magic number
const float kDamageThreshold = 0.025;
const int kVertexCount = 3;

int damagedVertexCount = 0;
bool damageState[kVertexCount];
bool isEdge[kVertexCount];
float damageValue[kVertexCount];

void main() 
{
	// Get initial triangle info
	for(int i = 0; i < kVertexCount; i++)
	{
		// Keep count of how many vertices lie on 
		// the edge of the mesh
		edgeCount += (aColor[i].z != 0) ? 1 : 0;	
		
		// Sample damage value per vertex
		damageValue[i] = texture(uDamageBuffer, aColor[i].xy).r;
		
		// Check if the vertex is damaged, keeping 
		// count of how many vertices are damaged
		damageState[i] = damageValue[i] > kDamageThreshold;
		damagedVertexCount += damageState[i] ? 1 : 0;
	}
	
	// Is this whole triangle destroyed?
	if (damagedVertexCount == kVertexCount)
	{
		// Discard triangle, don't emit any geometry
		return;
	}
	// Is this whole triangle undamaged?
	else if (damagedVertexCount == 0)
	{
		// Render flat triangle
		renderOriginalTriangle(false);	
		
		// Render flipped triangle
		renderOriginalTriangle(true);
		
		// If two vertices from this triangle lie 
		// on the edge of the mesh then we want 
		// to generate an extruded edge to cover the gap
		if (edgeCount > 1)
		{
			renderEdgeTrim();
		}
		
		return;
	}	
	
	//
	// At this point we know the triangle must be partially damaged
	//
	
	// This function moves the vertices into their new 
	// positions based on damageValue[] and re-calculates 
	// normals from these new positions.
	transformDamagedVertices();
	
	// Figure out where we need to generate a skirt to 
	// cover up partially damaged edges
	if (damagedVertexCount == 1)
	{
		// Find which vertex is culled
		int culledIndex = 
		(damageState[0] ? 0 : (damageState[1] ? 1 : 2));
		
		// Generate two skirt edges adjacent to the 
		// culled vertex renderSkirtEdge takes the 
		// indices of the two input vertices for which we 
		// want to draw the skirt
		if (culledIndex == 0)
		{
			renderSkirtEdge(1, 0);
			renderSkirtEdge(0, 2);
		}
		else if (culledIndex == 1)
		{
			renderSkirtEdge(1, 0);
			renderSkirtEdge(2, 1);
		}
		// (culledIndex  == 2)
		else
		{
			renderSkirtEdge(0, 2);
			renderSkirtEdge(2, 1);
		}
	} 
	// (damagedVertexCount == 2)
	else
	{
		// Build full skirt around all three edges 
		// of damaged triangle
		renderSkirtEdge(1, 0);
		renderSkirtEdge(2, 1);
		renderSkirtEdge(0, 2);
	}
	
	// Render using tranformed vertex positions 
	// from transformVertices()
	renderTransformedTriangle(false);
	
	// Render the same triangle flipped
	renderTransformedTriangle(true);
}


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

По сравнению с другими шейдерами, используемыми в игре, шейдер повреждений менее производителен, поэтому чем меньше ему требуется вершин, тем лучше. Используемая в игре плотность тесселяции («грани на метр») выбрана потому, что увеличение плотности не приводило к заметным графическим улучшениям. Это одно из преимуществ игр с камерой, сильно отдалённой от игрового процесса.

image-loader.svg


Рисунок 8. Повреждённая стена, какой она выглядит в игре, оверлей каркаса и нормали

5. Коллизии


Каждый раз, когда грань получает повреждения, необходимо обновлять collision shape. Это выполняется извлечением контура задействованной области из буфера повреждений и триангуляцией этого контура в новый collision shape.
Рисунок 9. Отрисовка в буфер повреждений и удаление из него при помощи инструментов редактора. Форма collision shape показана белым цветом.

Для анализа пиксельных данных копию буфера повреждений необходимо перенести из видеопамяти в память процессора. Это может быть очень медленной операцией, вызывающей простои в конвейере рендеринга. Чтобы избежать этого снижения производительности, можно выполнять запрос копии буфера повреждений сразу после применения повреждений, но не получать данные буфера данных до следующего кадра. Это позволяет графическому драйверу отложить операцию переноса на то время, когда перенос меньше будет влиять на производительность. Это означает, что collision shape будет иметь задержку в один кадр прежде чем начнёт соответствовать форме повреждённой грани, но это незаметно даже когда игра работает с низкой частотой кадров. Без использования этого способа при каждом повреждении стен количество кадров значительно снижалось бы.

Получение буфера повреждений из VRAM без простоев

// Damage event is triggered, damage sprite is drawn to damage buffer.
// ...

// Read pixels from damageBuffer into pixelBufferObject.
// Binding a PBO and calling glReadPixels with a null destination allows
// the driver to transfer the data into the PBO asynchronously, 
// preventing a stall.
glBindFramebuffer(GL_FRAMEBUFFER, damageBuffer);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixelBufferObject);

glReadPixels(
	0, 
	0, 
	bufferWidth, 
	bufferHeight,
	GL_RGBA, 
	GL_UNSIGNED_BYTE, 
	nullptr);
	
// Continue updating the rest of the frame
// ...

// Render, Swap
// ...

// -- New frame begins ----------------------

// Destructible wall entity is waiting for pixel data from the last 
// frame. When the wall entity gets updated it maps the pixel buffer 
// object and retrieves the pixel data.
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixelBufferObject);
unsigned char* pixels = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);

// Do work with pixels[]
// ...


Далее необходимо проанализировать пиксели буфера повреждений для извлечения контура повреждённой области. Я использую алгоритм marching squares для сканирования пикселей и нахождения краёв ярких фигур, обозначающих повреждённую область. Для этого требуется несколько магических значений порогов и эпсилон, которые были подобраны путём проб и ошибок. Это даёт нам множество 2D-фигур, края которых получены на основании координат пикселей. Эти фигуры затем упрощаются для устранения излишнего шума и деталей.

19b180475c712b55ff038d284c9c19fd.jpg


Рисунок 10. Генерирование нового collision shape из битовой карты

После сбора всей необходимой информации можно построить новый collision shape. Упрощённые фигуры передаются в библиотеку триангуляции, возвращающую множество треугольников, соответствующее контурам переданных фигур. Исходный нетронутый collision shape отбрасывается и создаётся новый, более сложный collision shape. Эта задача упрощается физической библиотекой, имеющей тип collision shape, который можно сгенерировать из списка треугольников.

Так как каждый из них ссылается на один и тот же исходный буфер повреждений, разрывы в триангулированном collision shape будут совпадать с разрывами в отображаемом меше. И теперь игрок может ходить сквозь стены!

6. Навигация


Для навигации NPC в Radio Viscera используется солвер A* на основе узлов. Узлы путей размещаются по уровню и автоматически соединяются на основе проверок видимости и доступности. Если NPC нужно переместиться в другое место, он запускает солвер путей между ближайшей к нужной точке узлом и узлом, ближайшим к текущему местоположению NPC.

image-loader.svg


Рисунок 11. Фиолетовыми линиями показан граф навигации, красная линия — прямой маршрут к конечной точке пути NPC, а зелёная — запланированный маршрут.

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

f1efad5f1adc823548b3ee1db67624f4.jpg


Рисунок 12. Вид сверху на граф навигации после события разрушения

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


Рисунок 13. Для демонстрации этого эффекта пролезание показано с замедленной в два раза скоростью

Из-за размера и формы тел коллизий персонажей некоторые персонажи часто застревают, пытаясь пролезть сквозь дыру, которая кажется достаточно большой для пролезания. Чтобы решить эту проблему, персонажи постоянно выполняют последовательность рейкастов для определения того, сталкиваются ли их головы со стеной, а также для проверки, не сталкиваются ли со стеной их средняя часть. Если это так, тогда персонаж знает, что он только стукнется головой о верхний край стены, поэтому должен пригнуться, чтобы пролезть. Когда эти условия выполняются, масштаб тела коллизии персонажа уменьшается по оси Y, чтобы он мог протиснуться в дыру. После того, как персонаж прошёл сквозь стену, рейкасты больше ничего не обнаруживают и тело коллизии возвращается к исходному масштабу. Во время этого процесса модель персонажа тоже уменьшает масштаб в соответствии с телом коллизии, чтобы имитировать приседание при пролезании в дыру.


Рисунок 14. NPC Bully даже не знает, что перед ним есть стена

Особым случаем навигации персонажей является мощный NPC по прозвищу «Bully», способный пробивать стены телом и подбрасывать игрока в воздух. Изначально я пытался реализовать его в обычной системе, при которой NPC врезается в стену, создаёт дыру, а затем проходит через новый путь. Такую систему сложно было заставить работать правильно — NPC разбивал стену на полной скорости, без остановки, менял направление или застревал. Решение заключалось в том, чтобы отключить коллизии между этим типом NPC и разрушаемыми стенами, продолжая при этом обнаруживать события коллизий и применяя повреждения к стене. Это создаёт иллюзию, что стена разрушена NPC и NPC может сохранять свою скорость в течение атаки. Кроме того, я изменил логику навигации этого NPC, чтобы он игнорировал разрушаемые стены при выполнении проверок видимости. Это позволяет NPC выполнять навигацию в конечную точку, не беспокоясь о выборе «правильного» пути.

7. Эффекты


Рисунок 15. Воспроизведение на 0,1 от обычной скорости

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


Рисунок 16. Эффект столкновения

Первый из них — это эффект столкновения из частиц. Он создаёт резкую вспышку и испускает очень яркие фрагменты от точки попадания вдоль нормали повреждений. Им придаётся большая начальная скорость с большим коэффициентом сопротивления воздуха, поэтому они быстро вспыхивают и мгновенно начинают замедляться. Меш каждой частицы — это обычный комковатый геометрический объект, тот же меш используется для фрагментов мусора, разбросанного по земле. Изначально каждая частица имеет цвет RGB [1600, 1600, 1600] и использует экспоненциальную функцию плавного изменения до [255, 255, 255], а её масштаб уменьшается до нуля. Срок жизни каждой частицы находится в интервале 0,5–4,0 секунды.


Рисунок 17. Эффект пыли

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

fd60fed6ffc557faf8282468c3d479b6.jpg


Рисунок 18. Режим Scene с соответствующим буфером смещения

Смещение в экранном пространстве используется для кратковременного искажения области вокруг точки попадания и усиления ощущения того, что это крупные, разрушительные, энергичные столкновения. При рендеринге сцены в отдельный буфер кадров рендерится низкополигональная сфера (Рисунок 18) специально для сущностей смещения. После завершения основного рендера сцены применяется стек постфильтров, среди которых ambient occlusion и depth of field. На вершине стека есть специальный постфильтр смещения в экранном пространстве, сэмплирующий буфер кадров смещений для искажения пикселей в готовой сцене. Сфера сначала имеет полный масштаб, но быстро сжимается, имитируя эффект отрицательного давления, возникающий после сильной взрывной волны. Масштабирование применяется на протяжении 0,5 секунды и использует экспоненциальную функцию плавного изменения, чтобы обеспечить быстроту и резкость.

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

Звуковой эффект, используемый при столкновении — это смесь нескольких сэмплов опрокидываемого песка и камней с басовой «бочкой». Это обеспечивает красивый низкий «бабах», который потом переходит в более высокие частоты для эффектов пыли и мусора.


Звуковой эффект осыпающегося камня

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


Звуковой эффект оружия

e9c75bc3f15ada0d4adaba340e1ab7c5.jpg


Рисунок 19. Соединение узлов в Reason, заставляющее все инструменты срабатывать одновременно

8. Двери


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

f96e81349ac2f2989cab06835eb5dde7.jpg


Рисунок 20. Проблемы с дверями на ранних этапах

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


Рисунок 21. Добавление двери в редакторе

На дальнейших этапах продакшена я отошёл от задания следов разрушаемых стен как прямоугольников, вместо этого использовав систему задания 2D-границ в 3D-пространстве (Рисунок 21, показаны зелёным). Одна из особенностей этого изменения заключалась в том, что можно было отключать части стены, которые не будут генерировать повреждаемую грань вдоль этого края. Я добавил инструменты, позволяющие мне «штамповать» дверь в стене, разделяя выбранную стену и вставляя дополнительную часть, у которой отключена генерация стены; по сути, это создаёт пустой пробел. Дверной проём помещается в этот новый пробел и над рамой добавляется фальшивый меш перемычки. Меш перемычки специально создан таким образом, чтобы он помещался в этот небольшой пробел и не имел коллизий, что сильно упрощает прохождение NPC через двери.


Рисунок 22. Избавляемся от всякого мусора

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

9. Примечания


Я не планировал эту систему тщательно с самого начала. Она постепенно вырастала из экспериментов с усечением граней при помощи вычислительных шейдеров и эволюционировала, пока превращалась в готовый продукт.
Рисунок 23. Один из первых тестов

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

5e84c0a5d4816d26b6204ae05c5a9e38.jpg


Рисунок 24. Сопоставить collision shapes с буфером повреждений было сложно

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

Сам геометрический шейдер тоже требует довольно много вычислений в каждом кадре, ведь он выполняет расчёты воссоздания позиций и нормалей повреждённых рёбер. Гораздо эффективнее было бы использовать вычислительные шейдеры для сканирования буфера повреждений и перестраивать меш только при изменениях в буфере повреждений. Однако проект разрабатывался для запуска в контексте OpenGL 3.3, в котором не поддерживаются вычислительные шейдеры.


Рисунок 25. Результат в игре

© Habrahabr.ru