Как только я довел небо до приемлемого качества, меня направили на доделывание взрывов. Оба моих приключения были внесены в мою жизнь предшественником, который очень быстро писал прототипы, но развивать их до нормального вида отказывался. С небом я потом вспомню, что было, доставшееся же мне разрывание моделей приводило к очень интересным визуальным эффектам. Например, взорваный камень превращался в... ну, от него оставались две его верхушки, связанные прежней формой столкновения (обычно, шаром). Такой очень прозрачный воздушный шарик с нарисованными на нем гранями камня в отдельных местах. И вот оно катится...
Идея руководителя проекта была такая: вместо рисования специальных взорванных моделей надо написать алгоритм разрыва на месте уже имеющихся. Алгоритм можно снабдить кучей параметров, специфичных для каждой модели (или даже типа персонажа). Это экономит время художников примерно в три раза. Художников было человек пять, моделей около полутора сотен, создание одной модели от дня до недели. Взрывы я писал два месяца.
Разрыв на месте означает, что мы берем модель персонажа, вот как она есть, с поднятой ногой и раскрытым ртом (условно, у нас персонаж был робот), проходимся по ней нашим алгоритмом, получаем несколько частей персонажа, на которые вешаем эффекты и которые начинаем разбрасывать взрывной волной.
Ну, я и приступил.
В качестве модели для взрыва бралась самая подробная модель, так называемый нулевой LOD (Level Of Detail). Ее надо было попытаться разбить на Nb больших кусков площадью больше Sb (в процентах от общей площади треугольников модели), Nm средних кусков площадью больше Sm и сколько получится маленьких площадью больше Ss.
Техническая подробность. Модель хранится в виде массива трехмерных точек с разными свойствами (положение, цвет, нормаль, координаты текстуры, индексы матрицы преобразования...) и массива индексов точек треугольников. Последний содержит число элементов, кратное 3, что разумно. К модели прилагается набор матриц преобразования - для точек предплечья, для точек плеча, всех пальце и прочих частей. Имея и то, и другое, мы можем получить текущее положение персонажа.
Первое, что я сделал, это сделал связность кусков.
Мой предшественник просто брал подряд идущие треугольники из массива индексов и создавал из них кусок взорванной модели. Треугольники в оригинальной модели лежат в произвольном порядке, поэтому и получался такой воздушный шар с картинками.
Я же создал граф связности треугольников и отбирал кандидатов по нему.
Граф связности треугольников можно создать двумя способами, по вершинам и по ребрам. По вершинам не подходит по понятным причинам - может получиться цепочка, что нехорошо. Остается связность по ребрам. При этом необходимо переименовать индексы точек треугольника, поскольку одна и та же точка в пространстве может принадлежать разным граням модели, иметь, допустим, разные нормали и, соответственно, иметь разные индексы в массиве трехмерных точек. Сделать это просто, надо при добавлении треугольника в граф посмотреть, нет ли среди его вершин точек с уже встречавшимися координатами. Если есть, то заменить их на индексы уже пройденных точек.
Для последнего я сделал систему хеширования трехмерных координат, что заметно ускорило работу.
Дальше все шло по следующему циклу:
- Добавить в текущий кусок взрыва треугольник.
- Убрать из просмотра ребра, оказавшиеся не на границе (от нуля до двух).
- Посчитать процент площади.
- Завершить цикл, если площадь больше разрешенного предела или нет соседей.
И делать это, пока есть треугольники.
Разрешенный предел зависит от количества уже созданных кусков - Sb, если не все кандидатуры на большие куски заполнены, Sm, если есть возможность создать средний кусок, Ss для оставшихся маленьких.
Вот мы и получили разорванную модель.
Теперь надо ее анимировать и отрисовать.
Для больших кусков применялась полномасштабная физика.
Для каждого большого куска создавался свой полноценный персонаж, со своей моделью, физическими параметрами (в моем случае - только массой) и формами проверки столкновений. Масса считалась, как масса исходного персонажа помножить на долю площади куска от общей площади персонажа. Получилось достаточно адекватно, именно по этой части придирок не было вовсе. ;)
Формы столкновений аппроксимировались параллепипедом по облаку точек куска. И оптимизировались - один параллепипед заменялся на один-три шара, если это было возможно. А это часто было возможно, например, у главного персонажа кабина была похожа на цилиндр. И еще парочка деталей получали шары в качестве форм столкновения. Конечно, надо было аппроксимировать не по всему облаку точек, а по его выпуклой оболочке, но и так неплохо получилось.
Игравший в демку сторонний народ достаточно долго катал по земле куски поверженных врагов. ;)
К большим кускам обычно добавлялся в качестве эффекта огонь или дым. Это стандартная штука движка, позволявшая приклеить к персонажу эффект.
Средние куски аппроксимировались точками.
Они отскакивали от стен и вращались, но отскакивали, как материальная точка, а при каждом столкновении я уменьшал и линейную скорость и угловую. Как только становилось возможно, я их замораживал, а через некоторое время и вовсе убирал, если игрок не имел возможности их видеть.
Маленькие куски не взаимодействовали с миром никак. Они просто создавали эффект и исчезали как можно быстрее.
Новопришедший товарищ высказался по этому поводу так: "Ух! Я ее взорвал, а она на меня ведро гаек кинула." ;)
В отрисовке важно было сократить количество DrawPrimitive (DP), вызовов отрисовки DirectX.
Большими кусками занимался движок, а средними и маленькими - я.
Я придумал вот, что. К каждому куску я приписывал последовательно возрастающий индекс матрицы преобразования. Как только индекс превышал максимальное для видеокарты значение, я его сбрасывал в 0. В шейдере отрисовки я применял эту матрицу к координатам точки, в цикле обновления я сжимал массив индексов, выбрасывая из него уже отжившие свое куски. Таким образом я мог отрисовывать некоторое количество кусков за один DP, столько, сколько могло влезть в память шейдера. Количество матриц, обычно, составляло не меньше 30-х, поэтому на взорванную модель главного героя, состоявшую из 27 тысяч треугольников у меня уходило 3-4 DP (супротив сотни без этого приема). А через пару секунд - и совсем 0, поскольку все куски свое отжили.
При создании взрыва сбрасывался шейдер отрисовки модели и все блестящие отражающие вещи становились тусклыми. Это получилось не нарочно, но придавало некий шарм - "лакировку взрывом сшибло." Я, было, заикнулся о сохранении шейдера, но поддержан не был.
Вот, практически все.
Осталась самая малость.
Для пущей художественности я создал многостадийные взрывы.
В каждой стадии указывалось время срабатывания, центр взрыва, его область действия и сила взрывной волны. Ну, и эффекты. Область действия захыватывала маленькие и средние куски и придавала им импульсы. Без импульса куски не начинали жить и не исчезали.
Взрыв главного героя выглядел так: сперва взрывалась тушка, разбрасывая в стороны гайки, шланги, кабину, двигатель и все такое, ноги оставались стоять, они были вне зоны взрыва. Затем через десятую долю секунды "взрывная волна доходила до поверхности под ногами" и срабатывала вторая стадия - между ног появлялся вихрь взрывной волны и ноги начинали разноситься ею.
Получилось достаточно впечатляюще. "О! Валенки начали разлетаться!"
Камни пришлось дополнять до замкнутой поверхности. Здесь получилось не так хорошо, как задумывалось, но вполне адекватно. Оказалось достаточно добавить всего одну точку.
Стены, состоящие из 6 сторон по два треугольника каждая взрывались не очень эффектно. Тогда конфигураторы сделали LOD для стен, который взрывался моим алгоритмом достаточно хорошо, и назначили ему область действия не далее 0.1 метра - то есть, он был не виден практически никогда. Это была единственная исправленная модель.
Десантника-человека те же самые тестеры-конфигураторы отконфигурировали так, что у него отлетала голова строго вверх, а тело оставалось стоять. Они даже ролик про это записали. ;)
Общая скорость была достаточно приличной - самая сложная модель главного героя разрывалась на куски за десятую часть секунды. Хоть это и был самый редкий случай в игре, кеш разорванных моделей (точнее, их массивов индексов) сделать пришлось. Создание взрыва стало совсем незаметным.
Кеш пришлось сделать аж два раза, поскольку главный программист был не уверен в том, что мой первый вариант не фрагментирует память. В результате во вторую реализацию вкралась ошибка типа "меньше или равно, а должно быть строго меньше" и игра изредка падала. Ошибку я искал довольно долго.
Кстати, общий размер кэша взрывов для всех моделей в игре составил, по-моему, порядка 10-15Мбайт.
Совсем забыл. У взрывов тоже были уровни детализации - на каком расстоянии не создавать мелкие детали, на каком не нужны даже средние, а в отдельных случаях - и вовсе ничего не делать, эффекты, может, показать, и все.