Экспорт анимированных моделей из Blender
главная страница статьи файлы о сайте ссылки
Экспорт анимации из Blender

Георгий Мошкин
tmtlib@narod.ru

Файлы к статье:
engine12.rar (482kb, exe+исходники+скрипт для Blender)
- пример передвижения модели по уровню
armatures12.rar (369kb, exe+исходники+скрипт для Blender) - новая версия экспортера
armatures.rar (390kb, exe+исходники+скрипт для Blender) - предварительная версия экспортера

Сначала рассмотрим вопрос о то, как был сделан экспортер анимированных моделей из Blender. Первым делом я раздобыл документацию по использованию скриптов Python в Blender -: "The Blender Python API Reference". Затем были проанализированы исходники разных экспортеров из директории скриптов Blender.

В одном из скриптов я нашёл замечательную функцию blender_bone2matrix(head, tail, roll). Эта функция преобразует Blender-овские параметры кости (head,tail,roll) в обычную матрицу. Необходимые параметры (head,tail,roll) в локальных координатах кости доступны только для "базового" положения скелета (ARMATURE), в котором сделана модель:

			head = realBone.head["BONESPACE"]
			tail = realBone.tail["BONESPACE"]
			roll = realBone.roll["BONESPACE"]

Так как же получить эти три параметра для кадров анимации? Оказалось, что это очень просто. Для этого достаточно переписать глобальные матрицы из интересующего нас кадра в основное положение скелета:

		Arm.bones[bonename].matrix=pose.bones[bonename].poseMatrix
		Arm.bones[bonename].head=pose.bones[bonename].head
		Arm.bones[bonename].tail=pose.bones[bonename].tail

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

		for bonename in sortednames:
			realBone = bones[realnumbers[bonename]]
			XFramematrix[bonename] = realBone.matrix['ARMATURESPACE']

А после каждого "прохода" экспорта мы восстанавливаем эти матрицы:

			Arm.makeEditable()
			for bonename in sortednames:
				Arm.bones[bonename].matrix = XFramematrix[bonename]
			Arm.update()

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

Теперь, когда у нас есть связка (head,tail,roll) для каждого кадра анимации, то мы без проблем находим три угла поворота и три перемещения (в стиле Half-Life smd):

			matrix = blender_bone2matrix(head, tail, roll)
			bonerestMat = m2b (matrix)
			bonepos = tail
			bonerot = bonerestMat.rotationPart().toEuler()

В итоге мы имеем вектор перемещения bonepos и три угла поворота (bonerot[0], bonerot[1], bonerot[2]) в локальной системе координат любой выбранной кости.

Для упрощения обработки скелета в нашем загрузчике я сделал сортировку. Сначала был найден индекс "нулевой" кости:

			boneidx = 0
			zeroboneidx = 0
			for bone in bones:
				if not bone.hasParent():
					zeroboneidx = boneidx
				realnumbers[bone.name]=boneidx;
				boneidx = boneidx + 1

Как видите, найти индекс нулевой кости просто: если кость ни к чему не прикреплена, то это и есть нулевая кость с индексом zeroboneidx. В далнейшем этот индекс будет использоваться для правильного начала работы алгоритма сортировки (смотрите устройство скрипта в файле nashformat.py).

Экспорт самой модели выполнен аналогично тому, как это делалось в статье "Создание экспортера моделей из Blender". К моменту начала модели у нас уже имеется массив с остортированными индексами костей sortednumbers. Нужно использовать именно этот массив, так как после сортировки наши индексы не совпадают с данными Blender:

   influences = mesh.getVertexInfluences(v.index)
   myfile.write("%i \n" % len(influences))

   sum = 0.0
   for bone_name, weight in influences:
   sum += weight

   for bone_name, weight in influences:
   myfile.write("%i %f \n" % (sortednumbers[bone_name], weight/sum))

Основная идея по загрузке данных из полученного при экспорте файла есть в статье "Загрузка 3D - модели в Delphi/OpenGL". В данной версии я сделал некоторые изменения в структуре данных: например, вершины теперь хранятся в отдельном массиве. А в треугольниках вместо вершин теперь хранятся индексы-ссылки на массив с вершинами. А так как соседние треугольники имеют очень много общих точек, то такая структура данных позволяет ускорить расчёты при скелетной анимации, так как не приходится трансформировать одну и ту же вершину по 3-4 раза.

Алгоритм отрисовки скелетно анимированной модели во многом схож с тем, что был описан в статье "Загрузка и проигрывание скелетной анимации в формате Half-Life SMD". Поэтому рассмотрим только учёт влияния весов костей и использование кватернионов. Начнём с кватернионов. Во-первых, важно понять, что кватернионы, вектора с углами, матрицы поворота, вращения и т.п. - это лишь способ представления данных. Между этими данными всегда можно произвести преобразования. То есть перейти от одних данных к другим. Кватернион нам понадобится лишь для того, чтобы произвести плавную интерполяцию углов вращения кости. Рассмотрим тип данных для хранения данных одной кости:

  type TBonePos=record
                 tra:TVector3s; вектор перемещения TRA и поворота ROT
                 rot:TVector3s; в локальной системе координат (с.к.) 
  
                 m_absolute:Matrix; матрица кости в глобальной с.к.
                 m_relative:Matrix; матрица в местной (локальной) с.к.
  
                 quat:Quaternion; кватернион поворота кости
  
                 vct:TVector3s; не используется
                end;

Итак, смотрите: в экспортере мы как раз записывали TRAnslation и ROTation для каждой кости в локальной системе координат. Из этих данных можно сделать матрицу m_relative:

   m_relative.SetRotationRadians(rot);
   m_relative.SetTranslation(tra);

А если пройтись по иерархии скелета, то с помощью перемножени матрицы m_absolute предыдущих костей на матрицы m_relative последующих мы можем получить m_absolute для всех матриц. Теперь что касается кватернионов: кватернион - это просто несколько чисел в массиве. Для переход от угла поворота к кватерниону существует готовая функция:

   quat.fromAngles(rot);

Как видите, из углов поворота (rot[0],rot[1],rot[2]) можно сделать элементы матрицы m_relative, соответствующие углам поворота. А можно сделать и кватернион. Просто конвертируем данные из одного типа в другой. Но так как матрица у нас "может делать много" (повороты, перемещения), то функция SetRotationRadians затронет лишь некоторые её элементы. Ещё раз повторю, что из файла с моделью мы tra[0],tra[1],tra[2] и rot[0],rot[1],rot[2]::

   tra:TVector3s; вектор перемещения TRA и поворота ROT
   rot:TVector3s; в локальной системе координат (с.к.) 

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

Рассмотрим применение кватерниона на практике. Допустим, у нас есть начальные NACH.X, NACH.Y, NACH.Z и конечные KON.X, KON.Y, KON.Z углы поворота. Для этих углов поворота можно сделать матрицы:

  NACH_MATR.SetRotationRadians(NACH);
  KON_MATR.SetRotationRadians(KON);

Теперь с помощью любой из этих матриц мы можем установить поворот кости KOST:

  KOST:=Transform(NACH_MATR,KOST);

В следующем кадре мы можем подставить матрицу KON_MATR, но тогда анимация будет "дёрганная". Что дают кватернионы? С помощью кватернионов мы можем получить промежуточные матрицы поворота между углами NACH и KON. Сначала сделаем сами кватернионы (кватернионы получаются из углов поворота):

   NACH_QUAT.fromAngles(NACH);
KON_QUAT.fromAngles(KON);

И сам поворот кости промежуточной матрицей:

   PROMEZHUTOCH_QUAT.slerp(NACH_QUAT, KON_QUAT, PERCENT)
PROMEZHUTOCH_MATR.setRotationQuaternion(PROMEZHUTOCH_QUAT);
KOST:=Transform(PROMEZHUTOCH_MATR,KOST)

Здесь PERCENT - это число от 0 до 1. Например, если сделать это число равным 0.5, то мы получим значение матрицы поворота PROMEZHUTOCH.MATR на половине пути от углов NACH до углов KON. Теперь вы можете делать даже эффекты с замедлением времени, как это было в игре MAX PAYNE. Для этого нужно просто сделать очень низкую скорость роста числа PERCENT между кадрами анимации.

При "сворачивании" модели веса костей учитываются следующим образом:

   for k:=0 to length(model.localized[i].parents)-1 do
    begin
     zzz:=model.vertex[i].coord;

     mmm:=model.skelanim.Actions[0].frames[0].data[
                          model.vertex[i].parents[k].bone
                                         ].m_absolute;

                      w:=model.localized[i].parents[k].weight;

     mmm.InverseTranslateVect(zzz);
     mmm.InverseRotateVect(zzz);

     fff[0]:=fff[0]+zzz[0]*w;
     fff[1]:=fff[1]+zzz[1]*w;
     fff[2]:=fff[2]+zzz[2]*w;
    end;

   model.localized[i].coord:=fff;
  end;

Смысл простой: если на вершину влияет несколько костей, то итоговое положение вершины оказывается в некоторой среденей точке между конечными положениями этой точки в случае влияния каждой из костей по отдельности. "Разворачивается" модель так же просто:

  for j:=0 to length(model^.localized)-1 do
    begin
       fff[0]:=0;
       fff[1]:=0;
       fff[2]:=0;

             for k:=0 to length(model^.localized[j].parents)-1 do
             begin
                     zzz:=model^.localized[j].coord;

                     mmm:=model^.SkelAnim.CurrentPose[
                                model^.localized[j].parents[k].bone
                                                       ].m_absolute;

                    w:=model^.localized[j].parents[k].weight;

                    zzz:=Transform(zzz,mmm);

                    fff[0]:=fff[0]+zzz[0]*w;
                    fff[1]:=fff[1]+zzz[1]*w;
                    fff[2]:=fff[2]+zzz[2]*w;

             end;


       model^.vertex[j].coord:=fff;

    end;

Каждый раз мы начинаем из точки с координатами (0,0,0). Координаты точки можно рассмотреть как вектор. В соответствии с весами костей мы производим наращивание вектора: (0+x1*w1+x2*w2+x3*w3, 0+y1*w1+y2*w2+y3*w3, 0+z1*w1+z2*w2+z3*w3). Здесь x1,y1,z1 - это координата, которую получила бы вершина, если на неё влияла только первая кость.

Понятия "сворачивания" и "разворачивания" весьма условные (см. статью "Загрузка и проигрывание скелетной анимации в формате Half-Life SMD"). Существует и немного другой подход к работе с моделью и костями, когда модель хранится в исходном (не "свёрнутом") состоянии, и к ней применяют произведения инверсных матриц костей в исходном положении на матрицы из анимированных кадров скелета.