Очень часто в компьютерных играх можно встретить такую ситуацию: Вы бродите по незнакомому городу, встречаетесь с другими персонажами, которые, вроде бы, должны быть разными, поскольку каждый человек индивидуален, однако все люди, живущее в этом городе, как будто только что сошли с конвейера. Раздражает. Чаще всего разработчики делают несколько типов персонажей и из ограниченного числа этих типов могут создать целый город или армию. Появляется некоторое разнообразие, но все равно - все одинаковые. В некоторых играх, перед началом игры, игроку предоставляется возможность выбрать некоторые характеристики персонажа, за которого он будет играть. При этом на выбор, могут быть предложены несколько физиономий (могут и не быть). Так или иначе, мне еще ни разу не понравились предлагаемые физиономии, какое бы их количество разработчики не предложили. Вот если бы можно было слепить физиономию своего персонажа, подобно тому, как сотрудники угрозыска создают фотороботом подозреваемого в преступлении...
В этой статье мы рассмотрим, как можно генерировать случайным образом лица персонажей, используя принцип фоторобота. Только мы будем его применять не к двухмерной, а к трехмерной графике.
Как известно, человеческое лицо можно воспринимать и как единое целое и как набор некоторых деталей. Детали лица, такие как нос, рот, глаза, уши и т.д., наиболее сильно определяют тип персонажа, поэтому для создания "случайной" физиономии, нам понадобится "эталонная" голова, к которой мы будем крепить детали из заранее созданных нами наборов. Степень детализации лица зависит от поставленной задачи. Если я создаю 3D action, в котором, главному герою придется по мере прохождения игры уничтожать целые армии, и при этом мне хочется, чтобы каждый солдат из стана врага имел собственные черты лица, то высокая степень детализации мне не нужна. Наоборот, для главного героя, лучше детализировать лицо по максимуму. Для рассмотрения в данной статье, я возьму первый вариант (он проще), то есть не слишком детализированную голову (малое количество полигонов и малое количество деталей лица).
Итак, "эталонная" голова будет выглядеть следующим образом:
Как видите, в ней мало полигонов (меньше 200), не обозначен рот, почти не обозначены глаза и т.д. Обычно в таких низкополигональных моделях мелкие детали обозначаются текстурой. Понятно, что заниматься созданием моделей должен профессиональный дизайнер, но для простого примера сойдет и то, что я приготовил. Теперь нам необходимо определить, какие части лица мы будет изменять и, соответственно, вершины, принадлежащие этим частям лица.
Итак, в основании шеи находятся вершины, которые мы вообще не должны трогать, поскольку в этом месте голова будет крепиться к телу. Вершины, принадлежащие носу, обозначены на рисунке ниже.
Для модификации щек будем использовать всего две вершины для каждой щеки.
Нижнюю челюсть и рот будем воспринимать, как единое целое.
Так же, как единое целое будем воспринимать лоб и глаза. Хотя можно бы было оперировать ими по отдельности, но так будет проще.
Шею тоже можно задействовать. Она может быть и тонкой и толстой, ее может не быть вообще.
Вершин, отвечающих за верхнюю часть головы, то есть за прическу, больше всего. Но вершины, отделяющие прическу от остальной головы лучше не трогать. На рисунке ниже все вершины, расположенные выше красной линии, мы будем перемещать для формирования прически, вершины, расположенные на линии, трогать не будем.
За что мы боремся в данном случае? В основном только за количество памяти, занимаемой созданными моделями. Модели, так или иначе, придется создавать, но мы можем сохранять только координаты тех вершин, которые были изменены (сдвинуты) по отношению к "эталонной" голове. Например, файл, получившийся в результате изменения носа, у меня получился таким:
Можно заметить, что здесь сохранены только четыре вершины, в то время как для носа у нас определено 10 вершин, то есть, сохранены только те вершины, которые были изменены. Делается это достаточно просто. Открываем файл с "эталонной" головой, сдвигаем вершины некоторой части лица, сохраняем в другой файл, потом сравниваем, результат сравнения сохраняем в еще один файл. Для работы с трехмерными моделями я использовал 3D Studio MAX, экспорт файлов осуществлял с помощью, собственноручно написанной на MAX Script'е, утилиты в ASCII-файлы.
В результате упорного, но совсем не долгого труда, у меня получился файл, содержащий "эталонную" голову, 6 вариантов щек, 5 вариантов лба, 7 вариантов причесок, 8 нижних челюстей, 6 вариантов шеи и 5 носов. Это не слишком много, но для примера вполне достаточно. В текстовом виде файл выглядит примерно следующим образом:
Первая строка в файле "numverts numfaces" сигнализирует о том, что следующая строка содержит две величины, определяющие количество вершин и граней соответственно. Далее следуют координаты вершин "эталонной" головы. Потом следуют грани. Каждая грань представлена тремя целыми числами, указывающими на номера вершин, образующих данную грань. Номера идут от единицы до numverts. После граней "эталонной" головы следуют данные о деталях лица. Сперва текстовая константа, указывающая на часть лица (cheek - щека), далее ключевое слово "vertices", затем число, определяющее количество измененных вершин. После этого идут непосредственно вершины, в виде:
индекс_вершины x y z
Заканчивается файл словом "end". В текстовом виде все данные заняли 16 килобайт. Это не много.
Перейдем теперь к рассмотрению примера. Для его компиляции потребуется среда Delphi 3,4,5. Для работы необходима библиотека Glut. Я использовал Glut 3.6. Я постарался по максимуму насытить исходные тексты комментариями, поэтому подробно вдаваться не буду, кроме того, я рассчитываю на более менее продвинутых читателей и здесь более важен принцип, нежели исходный код.
Ключевыми функциями программы являются две функции MakeMorda и SetMorda. Первая достаточно проста - подбирает случайным образом части лица.
Здесь есть только небольшой нюанс - если для части лица подобран номер 0, то эта часть лица остается такой же, как у "эталонной" головы. Процедура SetMorda тоже не сложна для восприятия (да и вообще, весь пример достаточно прост).
procedure SetMorda(AMorda: TMorda);
var
FacePart: PFacePart;
procedure SetPart
var
i: Integer;
beginfor i := 0to FacePart.VertexCount - 1do
Head.DrawVertices[FacePart.Vertices[i].Index] :=
FacePart.Vertices[i].Vertex;
end;
begin
Move(Head.Vertices^,Head.DrawVertices^,
Head.VertexCount*SizeOf(TVertex3));
if AMorda.nNose >0thenbegin
FacePart := Head.Noses[AMorda.nNose - 1];
SetPart;
end;
..............................................
if AMorda.nLowJaw >0thenbegin
FacePart := Head.lJaws[AMorda.nLowJaw - 1];
SetPart;
end;
CalcNormals(Head.FaceCount,
Head.DrawVertices,
Head.Faces,
Head.Normals);
CalcSmoothVectors(Head.FaceCount,Head.VertexCount,
Head.Faces,Head.Normals,
Head.SVectors);
end;
Сначала "эталонная" голова возвращается на место. То есть участок памяти, содержащий вершины эталонной головы, копируется в тот массив, который отображается на экране в каждом кадре. Далее, для каждой части лица, если выбранный номер больше нуля, подпроцедурой SetPart происходит установка этой части лица с выбранным номером. То есть, вершины данной части лица копируются в отображаемый массив в соответствии со своими индексами. По идее, после изменения положения вершин, неплохо бы выполнить перерасчет нормалей и векторов сглаживания, что я и делаю. Однако если эти строки закомментировать, то картинка, на глаз, не меняется. Учитывая, что расчет нормалей использует функцию квадратного корня, лучше делать его как можно реже.
Описанный принцип можно использовать разными способами. Например, можно хранить одну "эталонную" голову в памяти, а для каждого персонажа в игре хранить лишь номера частей лица. В этом случае процедуру SetMorda (или как там она будет у Вас называться) потребуется вызывать при рендеринге каждого персонажа, и тогда лучше избавиться от перерасчета нормалей и векторов сглаживания. Другой способ более производителен, но потребует большего расхода памяти, подразумевает генерацию голов для всех, еще живых, персонажей и хранение их в памяти, пока пациент жив.
Да, кстати, управление. F1, F2 - масштаб, F5 - новая голова, стрелки курсора - вращать.
Ну вот, пожалуй, и все. Есть еще, правда, вопрос - а как быть с текстурами? Ну, если кому не очевидно, то я позже про них напишу. С текстурами тоже можно поиграться.
Ну и напоследок, сейчас я работаю над книгой, посвященной разработке компьютерных игр. Работа идет не быстро, а сделать надо очень много. Я ищу соавторов, заранее попрошу "начинающих" не беспокоиться, а для остальных, кого это заинтересовало, вот адрес: . Также, сюда могут писать те, кто что-то не понял в статье или исходных кодах.