Войти
ПрограммированиеСтатьиГрафика

Мультимординг.

Автор:

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

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

Как известно, человеческое лицо можно воспринимать и как единое целое и как набор некоторых деталей. Детали лица, такие как нос, рот, глаза, уши и т.д., наиболее сильно определяют тип персонажа, поэтому для создания "случайной" физиономии, нам понадобится "эталонная" голова, к которой мы будем крепить детали из заранее созданных нами наборов. Степень детализации лица зависит от поставленной задачи. Если я создаю 3D action, в котором, главному герою придется по мере прохождения игры уничтожать целые армии, и при этом мне хочется, чтобы каждый солдат из стана врага имел собственные черты лица, то высокая степень детализации мне не нужна. Наоборот, для главного героя, лучше детализировать лицо по максимуму. Для рассмотрения в данной статье, я возьму первый вариант (он проще), то есть не слишком детализированную голову (малое количество полигонов и малое количество деталей лица).

Итак, "эталонная" голова будет выглядеть следующим образом:

Изображение

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

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

Изображение

Для модификации щек будем использовать всего две вершины для каждой щеки.

Изображение

Нижнюю челюсть и рот будем воспринимать, как единое целое.

Изображение

Так же, как единое целое будем воспринимать лоб и глаза. Хотя можно бы было оперировать ими по отдельности, но так будет проще.

Изображение

Шею тоже можно задействовать. Она может быть и тонкой и толстой, ее может не быть вообще.

Изображение

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

Изображение

За что мы боремся в данном случае? В основном только за количество памяти, занимаемой созданными моделями. Модели, так или иначе, придется создавать, но мы можем сохранять только координаты тех вершин, которые были изменены (сдвинуты) по отношению к "эталонной" голове. Например, файл, получившийся в результате изменения носа, у меня получился таким:

 
nose4.dat
Vertices
4
10 3,550910 -0,908054 -2,766790
17 3,533760 1,035670 -2,753050
93 4,186350 -0,136389 -1,642700
94 4,206480 0,236281 -1,662180

Можно заметить, что здесь сохранены только четыре вершины, в то время как для носа у нас определено 10 вершин, то есть, сохранены только те вершины, которые были изменены. Делается это достаточно просто. Открываем файл с "эталонной" головой, сдвигаем вершины некоторой части лица, сохраняем в другой файл, потом сравниваем, результат сравнения сохраняем в еще один файл. Для работы с трехмерными моделями я использовал 3D Studio MAX, экспорт файлов осуществлял с помощью, собственноручно написанной на MAX Script'е, утилиты в ASCII-файлы.

В результате упорного, но совсем не долгого труда, у меня получился файл, содержащий "эталонную" голову, 6 вариантов щек, 5 вариантов лба, 7 вариантов причесок, 8 нижних челюстей, 6 вариантов шеи и 5 носов. Это не слишком много, но для примера вполне достаточно. В текстовом виде файл выглядит примерно следующим образом:

 
numverts numfaces
   95        179
Vertices:
  -3.20949 -1.59945 -1.30448
    ..........................
Faces:
  3 1 2
    ........
  74 76 80
cheek:
Vertices:
2
4 2.087400 -1.904220 -3.014110
18 2.001200 2.025030 -3.106330
cheek:
Vertices:
.............
nose:
Vertices:
7
10 3.550910 -0.908054 -2.766790
11 4.795330 -0.143358 -2.271300
12 4.808630 0.256833 -2.258230
17 3.533760 1.035670 -2.753050
92 4.205810 0.070365 -1.636600
93 4.186350 -0.136389 -1.642700
94 4.206480 0.236281 -1.662180
end

Первая строка в файле "numverts numfaces" сигнализирует о том, что следующая строка содержит две величины, определяющие количество вершин и граней соответственно. Далее следуют координаты вершин "эталонной" головы. Потом следуют грани. Каждая грань представлена тремя целыми числами, указывающими на номера вершин, образующих данную грань. Номера идут от единицы до numverts. После граней "эталонной" головы следуют данные о деталях лица. Сперва текстовая константа, указывающая на часть лица (cheek - щека), далее ключевое слово "vertices", затем число, определяющее количество измененных вершин. После этого идут непосредственно вершины, в виде:

индекс_вершины x y z

Заканчивается файл словом "end". В текстовом виде все данные заняли 16 килобайт. Это не много.

Перейдем теперь к рассмотрению примера. Для его компиляции потребуется среда Delphi 3,4,5. Для работы необходима библиотека Glut. Я использовал Glut 3.6. Я постарался по максимуму насытить исходные тексты комментариями, поэтому подробно вдаваться не буду, кроме того, я рассчитываю на более менее продвинутых читателей и здесь более важен принцип, нежели исходный код.

Изображение

Ключевыми функциями программы являются две функции MakeMorda и SetMorda. Первая достаточно проста - подбирает случайным образом части лица.

 
procedure MakeMorda(var AMorda: TMorda);
begin
  AMorda.nNose := Random(Head.Noses.Count + 1);
  AMorda.nNeck := Random(Head.Necks.Count + 1);
  AMorda.nCheek := Random(Head.Cheeks.Count + 1);
  AMorda.nForeHead := Random(Head.ForeHeads.Count + 1);
  AMorda.nHair := Random(Head.Hairs.Count + 1);
  AMorda.nLowJaw := Random(Head.lJaws.Count + 1);
end;

Здесь есть только небольшой нюанс - если для части лица подобран номер 0, то эта часть лица остается такой же, как у "эталонной" головы. Процедура SetMorda тоже не сложна для восприятия (да и вообще, весь пример достаточно прост).

 
procedure SetMorda(AMorda: TMorda);
var
  FacePart: PFacePart;
 
  procedure SetPart
  var
    i: Integer;
  begin
    for i := 0 to FacePart.VertexCount - 1 do
      Head.DrawVertices[FacePart.Vertices[i].Index] :=
                        FacePart.Vertices[i].Vertex;
  end;
 
begin
  Move(Head.Vertices^,Head.DrawVertices^,
       Head.VertexCount*SizeOf(TVertex3));
  if AMorda.nNose > 0 then begin
    FacePart := Head.Noses[AMorda.nNose - 1];
    SetPart;
  end;
 
  ..............................................
 
  if AMorda.nLowJaw > 0 then begin
    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 - новая голова, стрелки курсора - вращать.

Ну вот, пожалуй, и все. Есть еще, правда, вопрос - а как быть с текстурами? Ну, если кому не очевидно, то я позже про них напишу. С текстурами тоже можно поиграться.

Исходный код: 20021116.zip

Ну и напоследок, сейчас я работаю над книгой, посвященной разработке компьютерных игр. Работа идет не быстро, а сделать надо очень много. Я ищу соавторов, заранее попрошу "начинающих" не беспокоиться, а для остальных, кого это заинтересовало, вот адрес: . Также, сюда могут писать те, кто что-то не понял в статье или исходных кодах.

Всегда Ваш, Иван Дышленко.

#Delphi

18 февраля 2002 (Обновление: 17 июня 2009)