3DS Max - Экспорт скелетной анимации

 

Добрый день уважаемые читатели!

Сегодня мы с Вами будем писать собственный экспортер скелетной анимации (Skinned Mesh) из 3DSMax. Многие из Вас конечно могут возразить – «а зачем?, ведь есть же Collada и иже с ней?». Да, есть, и ее вполне можно с успехом использовать. Но зачем вгонять себя в рамки какого-либо формата? А если завтра формат изменят? Или прекратят поддержку? Или Вам будут нужны дополнительные «фичи» ? И даже если это Вас не убедило – то знать и уметь писать плагины для 3DSMax – очень неплохой бонус в резюме :) Итак, если никто не против мы начнем…

Для начала нам понадобится:

  • 3DSMax версии не ниже 9
  • 3DSMax SDK (идет в поставке с полной версией 3DSMax)
  • Visual Studio версии не ниже 2003
  • мозг, прямые руки, пиво, чипсы (продолжить список по вкусу)

Я при написании статьи использовал 3DSMax 2009 x64 + Visual Studio 2005 Pro SP1.

Надеюсь что установить 3DSMax проблемы не составит (вопросы лицензирования я опускаю). Перейдем к установке и настройке 3DSMax SDK. Скорее всего установщик 3DSMax SDK будет находится в директории support\SDK\ на диске с самим 3DSMax. Запустите установщик и проследуйте инструкциям. Теперь SDK надо настроить для удобства пользования. Перейдем в директорию куда установили SDK, зайдем в директорию howto и в ней в 3dsmaxPluginWizard. Теперь любым текстовым редактором (Блокнотом например) отредактируем файл 3dsmaxPluginWizard.vsz. Замените в нем строку Param="ABSOLUTE_PATH = [Absolute Path Location of 3dsmaxPluginWizard Root Directory]" на полный путь к этой директории, например: Param="ABSOLUTE_PATH = F:\Development\SDKs\Autodesk\3dMax2k9_SDK\maxsdk\howto\3dsmaxPluginWizard". Теперь скопируйте файлы  3dsmaxPluginWizard.ico, 3dsmaxPluginWizard.vsdir, 3dsmaxPluginWizard.vsz в директорию VC\vcprojects находящуюся в директории куда Вы установили Visual Studio, например C:\Program Files\Microsoft Visual Studio 8\VC\vcprojects. Поздравляю! Мастер создания плагинов для 3DSMax успешно интегрирован в Visual Studio.

Теперь перейдем к созданию плагина. Запустите Visual Studio, выберите File->New->Project… и в разделе Visual C++ выберите «3ds max Plugin Wizard»

Обзовите как-нибудь проект и нажмите OK. Появится окно мастера 3DS max Plugin Wizard. Выберите нужный тип плагина, в нашем случае это «File Export 3DXI» и нажмите Next.

В следующем окне нам предлагают обозвать главный класс нашего плагина, выбрать базовый класс (в нашем случае это «SceneExport»), ввести категорию плагина и его описание.

Наконец в последнем окошке нам предлагают ввести полные пути к: директории куда установлен 3DSMax SDK, директории куда будет копироваться наш плагин и директории где находится 3dsmax.exe. Заполняем и жмем Finish. Ура! Проект создался и он успешно компилируется.

 

Итак, в первую очередь нам нужно заполнить следующие методы нашего класса:

const TCHAR *GWExport::Ext(int n)

этот метод должен возвращать i-е расширение поддерживаемое нашим плагином. В нашем случае расширении всего одно, поэтому реализуем метод примерно так:

const TCHAR *GWExport::Ext(int n)
{
    if ( !n )
        return _T("GWM");
    else
        return _T("");
}

методы

const TCHAR *GWExport::LongDesc()

и

const TCHAR *GWExport::ShortDesc()

возвращают соответственно длинное и короткое описания нашего плагина.

метод

const TCHAR *GWExport::AuthorName()

информирует пользователей об авторе сего произведения :)

метод

const TCHAR *GWExport::CopyrightMessage()

как ясно из его названия, возвращает страшный копирайт.

методы

const TCHAR *GWExport::OtherMessage1()
const TCHAR *GWExport::OtherMessage2()

мне непонятны, я их оставил пустыми :)

метод

unsigned int GWExport::Version()

возвращает версию плагина * 100. (т.е. v1.0 == 100, v1.07 == 107)

главный же метод нашего класса есть

int GWExport::DoExport(const TCHAR* name,ExpInterface* ei,Interface* i, BOOL suppressPrompts, DWORD options)

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

IGameScene * pIgame = GetIGameInterface();

Теперь нужно определиться с системой координат. По умолчанию в 3DSMax используется правосторонняя система координат с осью Z направленной вверх. Благодаря тому что мы решили использовать IGame, мы можем не беспокоиться о конвертировании координат, а воспользоваться интерфейсом IGameConversionManager для того чтобы указать в какую систему координат IGameScene должен подготовить нам данные. Делается это примерно так:

IGameConversionManager* cm = GetConversionManager();
cm->SetCoordSystem( IGameConversionManager::IGAME_OGL );

теперь мы можем попросить IGameScene собрать для нас информацию о сцене и перевести ее в удобный нам формат:

pIgame->InitialiseIGame();

Перед тем как перейти дальше немного оговоримся об ограничениях – в данной реализации плагин рассчитан на то, что в сцене находиться всего один меш к которому прилинкован скелет (Physique не поддерживается). Также отсутствует какая-либо оптимизация анимационного трека, захватываются все ключевые кадры (Key-Frame Animation).

Для начала попросим IGameScene дать нам все ноды типа «меш».

Tab<IGameNode*> meshes = pIgame->GetIGameNodeByType( IGameObject::IGAME_MESH );

Для того чтобы мы могли добывать необходимые нам данные из нода, нужно получить его IGameObject.

IGameObject* obj = node->GetIGameObject();
obj->InitializeData();

Что бы не расписывать сейчас весь код плагина (а его много) просто расскажу немного о тех классах которые я написал для упрощения жизни.

Класс Sceleton занимаеться тем что добывает из сцены кости прилинкованные к нашему мешу, переводит их в удобный нам формат и строит их иерархию.

Класс AnimTrack занимается тем что захватывает ключевые кадры анимации и для каждой кости вычисляет относительную матрицу (у нас ведь Hierarhical Skinned Mesh, не так ли :) ).

Наконец класс ExportMesh собирает данные о геометрии меша (вершины, текстурные координаты, нормали и веса костей для вершин).

Здесь кстати есть один нюанс. В 3DSMax точка 0,0 на текстуре находиться в левом верхнем углу, в OpenGL эта точка находится в левом нижнем углу, поэтому при экспорте текстурных координат мы переворачиваем текстурную координату ‘y’:

v.SetTexCoord( uvw.x, 1.0f - uvw.y );

С использованием этих классов код экспорта становиться прост до неприличия:

std::stringstream ss;
Skeleton    skeleton;
ExportMesh  expMesh;
AnimTrack   animTrack;
 
// iOrange - capture bones and build hierarhy
if ( !skeleton.Build( obj ) )
      goto export_ends_here;
// iOrange - capture mesh data
if ( expMesh.Build( obj, node ) != ExportMesh::RET_OK )
      goto export_ends_here;
// iOrange - capture per-vertex bones influence
if ( !expMesh.CaptureVertexWeights( obj, &skeleton ) )
      goto export_ends_here;
// iOrange - capture bones transformation on each frame
if ( !animTrack.Build( obj, &skeleton ) )
      goto export_ends_here;
 
skeleton.WriteToStream( ss );
expMesh.WriteToStream( ss );
animTrack.WriteToStream( ss );

Итак, анимация отэкпорчена в файл, теперь самое время ее отобразить.

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

Итак у нас есть скелет в котором иерархически лежат кости. Вершиной иерархии есть «корневая кость» (Root Bone) к которой уже цепляются дочерние кости. Для каждой кости хранится относительная трансформация. То есть трансформация относительно родительской кости. Для чего это нужно? Это дает много преимуществ. Например мы можем воздействовать на плечную кость, и ее дочерние кости тоже повторят это за ней, и причем абсолютно «бесплатно» ! Они даже знать не будут о том что кто-то трогает «плечо», просто они привязаны к ней. Разве это не прекрасно? Например можно реализовать Rag-Doll.

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

Также хочу заметить что просмотрщик написан с использованием OpenGL 3.0, так что если Вам что-то будет непонятно, обратитесь к статье Знакомство с OpenGL 3.0.

Как всегда в итоге вы должны увидеть такую картину:

Всем спасибо за внимание и успехов в освоении 3DSMax'а.

PS. Как обычно напоминаю – оставайтесь с нами! В скором времени продолжение темы скелетной анимации – мы с Вами будем переносить расчет анимации в шейдер.

Исходный код к статье:
ExportAnim.zip (2815)

  7 Ответов в “3DS Max - Экспорт скелетной анимации”

  1. Друг, подскажи если не трудно - написал свой экспортер из 3 ds Max (DLL). Все экспортируется правильно - вершины, нормали и т.д. За исключением текстурных координат !!! Пример - 8 вершин - 20 текстурных координат... Использую для загрузки модели Вершинный и Индексный буферы (DirectX). Профессиональные экспортеры (типа "Panda") экспортируют правильно - сколько вершин, столько же и текстурных координат. Написал создателю "Panda", но пока нет ответа. Если надо, пришлю исходники. Не оставь мой вопрос без ответа!!!

    • Здравствуйте Владимир!

      Если текстурных координат больше чем вершин - это вполне нормально. Давайте представим куб: верхняя и фронтальная грани буду иметь 2 общих вершин, но текстурные координаты в этих вершинах для каждой грани могут быть разными. 3Ds Max оптимизирует геометрию - поэтому вершин меньше чем текстурных координат. Вам нужно всего лишь продублировать эти вершины - будет несколько вершин с одинаковой позицией но разными текстурными координатами. И перестроить индексный буфер.
      Из примера выше Вы можете позаимствовать класс ExportMesh. И конкретно в методе int Build(IGameObject* obj, IGameNode* node) можете увидеть m_VertexList.Add(v); - если такая вершина уже существует, здесь просто вернется индекс существующей вершины. В противном случае - вершина добавится в буфер и вернется ее индекс.

  2. По вашему примеру написал свой плагин, но при экспорте кадров анимации получается какаято белеберда. Код - копипаст, модель из примера (и в ней (в arab.gwm) совсем другие данные)... может что упустил...
    Вот например первый фрейм:
    0_f: (16.045204 -0.000002 0.000001) (-0.000000 0.108783 0.000000 0.994066)
    0_f: (16.045204 -0.000001 0.000001) (-0.000642 -0.139762 0.020468 0.989973)
    0_f: (-3.530117 3.294905 -0.670506) (-0.040292 0.102049 -0.993171 0.039680)
    0_f: (-17.744583 35.441605 -18.209213) (-0.420756 -0.326420 -0.565279 0.629979)
    0_f: (-3.812694 -3.088860 -0.070941) (-0.056095 -0.233465 -0.970729 -0.005718)
    0_f: (3.691413 0.024546 0.011253) (-0.047780 -0.048191 -0.019761 0.997499)
    0_f: (16.045200 0.000001 0.000002) (-0.000000 0.103044 0.000000 0.994677)
    0_f: (16.045204 0.000001 0.000000) (-0.000039 -0.098676 -0.008813 0.995081)
    0_f: (5.210206 0.000001 0.000001) (-0.093890 -0.004083 -0.029106 0.995149)
    0_f: (-0.034701 1.117403 0.104042) (-0.673988 -0.737906 0.010763 -0.033454)
    0_f: (0.034702 -1.117403 -0.112340) (0.705498 -0.707418 -0.042697 -0.003216)
    0_f: (14.262310 0.000401 0.004120) (0.047692 0.032659 0.017037 0.998183)
    0_f: (4.171754 0.000000 0.000001) (-0.055782 0.196523 0.540825 0.815951)
    0_f: (4.171751 0.000004 0.000001) (0.053774 0.084409 -0.589842 0.801293)
    0_f: (13.999105 0.000002 -0.000002) (0.000000 0.389536 -0.000000 0.921011)
    0_f: (10.434709 -0.000002 0.000002) (-0.705906 0.020718 -0.015319 0.707837)
    0_f: (13.999108 0.000001 -0.000003) (-0.000000 0.292989 -0.000000 0.956116)
    0_f: (10.434701 0.000002 0.000000) (0.713397 0.029721 0.028130 0.699564)

  3. Подскажите, что может быть не так. Запускаю, а экран черный. Тоже самое с примером в этой статье http://3d-orange.com.ua/skining-in-shader-glsl/

  4. Подскажите, можно как-то сделать, чтобы количество вершин, которое изначально было в меше, не увеличивалось после экспорта?

    • Здравствуйте. Проблема в том, что одной и той же вершине может соответствовать несколько нормалей и/или текстурных координат. Представте себе куб - визуально вершин у куба всего 8, однако нормалей столько, сколько у него сторон, поэтому приходится дублировать некоторые вершины отличающиеся атрибутами. Можно конечно избежать дублирования вершин разложив вершинные атрибуты в разные буфера и сгенерировав несколько индексных буферов, однако этот способ неэффективен.

  5. Спасибо за статьи. Это один из немногих сайтов, где есть действительно полезная информация для создания игр. а не нарисуй треугольник, квадрат и т. д. с нулевым выходом в итоге. Подскажите как решить возникшую проблема. Пытаюсь добавить камеру в пример с просмотрщиком.При добавлении камеры и комментировании строк
    proj = MakePerspective( 60, float(_WND_WIDTH)/float(_WND_HEIGHT), 0.1f, 1000 );
    mv = MakeLookAt( _vec3(-50, 100, -90), _vec3(0, 0, 0), _vec3(0, 1, 0) );
    mvp = proj * mv;
    glext::glUniformMatrix4fvARB( _modelViewProjection, 1, 0, mvp );

    приводит к тому, что модель отображается без текстуры. Шейдеры знаю пока слабо, был бы очень признателен за подсказку

Оставить комментарий на iOrange Отменить ответ

(required)

(required)

Вы можете использовать HTML теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2011 3D-Orange.com.ua
e-mail me

3D-Orange.com.ua is proudly powered by WordPress.
Suffusion theme by Sayontan Sinha