Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Создание Export плагина для 3D Studio MAX

Автор: Роман Марченко
Источник: www.gamedev.ru

Построение плагина

Лирическое отступление.

Все, наверняка, учились геймдевелоперскому делу, пробуя писать, пусть небольшую, но собственную игру. В процессе разработки оттачивали полученные в процессе чтения книг и статей навыки. Начинают обычно с достаточно простых вещей: система частиц, менеджер ресурсов, подсистема ввода, вывод текста. Но, в конце-концов, наступал момент, когда на экране хотелось видеть что-то более красивое, чем затекстурированный кубик вращающийся в центре. :) Каждый выходил из положения по-своему. Кто-то качал из Интернета готовый лоадер популярного формата 3D файлов, кто-то писал сам, а самые настойчивые, рвущиеся к знаниям и большему опыту геймдевелоперы, решали создать собственный экспортер из одного из распространенных  3D редакторов. Естественно (никто и не утверждает обратного), тем, кто только начинает, за достаточно сложное дело написания экспортера, браться не стоит. А вот для тех, кто уже уверен в своих силах, и готов попробовать их на новом поприще, данная статья может стать хорошей базой для воплощения своих задумок в жизнь. Как можно понять из заголовка, я выбрал в качестве «подопытного» 3D Studio MAX.

Системные требования.

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

1.  Установленный 3D Studio MAX 5.0 c SDK. Учтите, что типичная установка MAX'а не включает SDK. Замечу также, что данный код наверняка, будет работать и с Максом начиная от версии 3.0, но необходимо знать, что плагины скомпилированные с разными версиями SDK не совместимы, т.е. плагины к 5-му максу не будут работать с 4-м и т.д.

2.  Visual Studio версии 6.0 и выше. Пример написан мною под VC++ 6.0, плюс все подготовительные процедуры я буду описывать, неявно ссылаясь именно на эту версию среды.

3.  Достаточно твердое знание C++ и среды разработки. Тут, я думаю, комментарии излишни. :)

4.  Поверхностное знание 3D Studio MAX.

5.  Настоятельно рекомендуется прочесть сначала статью: "Основы плагиностроения к 3D Studio MAX". Я в тексте многое упомяну, но более поверхностно.

6.  Как говорит англоязычное население земли, at last but not at least: настойчивость и любознательность. Без этого совсем никак.

Общие сведения и подготовительные операции.

И вот мы добрались, собственно, до плагинов. 3D Studio MAX (далее просто MAX) SDK позволяет создавать великое множество разнообразных типов плагинов. Среди них такие как: рендеры, модификаторы, плагины материалов,  текстур и атмосферы, и, конечно же, импортеры и экспортеры геометрии. Вот, последним типом мы сегодня и займемся вплотную. Все плагины представляют собой обычную dll которая экспортирует несколько функций. Для каждого типа плагина предусмотрено свое расширение для имени файла, именно так МАХ распознает их тип во время загрузки. Для экспортеров предопределено расширение <.dle>.

Итак начнем с создания проекта пустой dll. Перво-наперво, нам требуется сконфигурировать проект для дальнейшей работы. Известно, что по умолчанию VC++ создает 2 конфигурации сборки проекта. Это Release и Debug. Для успешной компиляции и работы необходимо сделать кое-какие изменения.

Начнем с Release. Единственное, что жизненно необходимо поменять, это во вкладке C/C++, Category->Code Generation, Use run-time library: выбираем Multithreaded DLL, вместо просто Multithreaded. Если этого не сделать, то ваш плагин будет крашиться с сообщениями об ошибках доступа к памяти.

Теперь Debug. Здесь немного сложнее. Проблема в том, что Debug конфигурацию проекта могут использовать только зарегистрированные разработчики, у которых есть специальная Debug версия МАХ'а (с которой, кстати, идет в поставке специальная версия SDK). У нас же, простых смертных, установлена обычная, Release версия 3d студии (я в этом уверен, так как если бы было иначе, ты не читал бы это сейчас :) ), так что мы сделаем небольшой финт ушами, для того чтобы можно было отлаживать наши наработки. Необходимо создать новую конфигурацию проекта под названием Hybrid, путем копирования Debug конфигурации. Далее необходимо заменить, также как и в Release, Use run-time library: с Debug Multithreaded, на Multithreaded DLL. И не забудьте во вкладке General конфигурации Hybrid заменить содержимое полей Intermediate files, и Output files на Hybrid. Вот и все.

HINT: во всех конфигурациях удобно указать в поле Link->Output file name полный путь с именем плагина в папку stdplugs МАX'a. У меня, например, так:
c:\program files\3Dmax\stdplugs\MyExpPlg.dle. Это избавит от копирования собранной библиотеки в папку, где размещаются все плагины редактора.

Теперь подключаемые библиотеки. Для успешной компиляции необходимо подключить: comctl32.lib (Common Controls), core.lib, maxutil.lib, geom.lib и mesh.lib. Последние 4 лежат в каталоге maxsdk\lib\. Core и Maxutil обязательные для всех типов плагинов.

Подготовка окончена, теперь можно приступать к программированию.

Базовый каркас.

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

Каждая dll плагина должна обязательно реализовывать следующие функции:

DllMain(HINSTANCE hinstDLL,ULONG fdwReason,LPVOID lpvReserved);

LibNumberClasses();
LibClassDesc(i);
LibDescription();
LibVersion();

DllMain();

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

BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
  hInstance = hModule;
  if (!ControlsInit)
  {
    ControlsInit = true;
    InitCustomControls(hInstance);
    InitCommonControls();
  }
    return TRUE;
}

LibNumberClasses().

Разве я вам еще не говорил, что в одной библиотекой можно реализовать несколько плагинов? Так вот, эта функция возвращает количество плагинов в dll. Типичная реализация:

__declspec(dllexport) int LibNumberClasses()
{
  return 1;
}

LibClassDesc().

МАХ, должен знать какой тип плагина содержится в библиотеке, так же необходимо знать некоторый другие параметры для нормального функционирования. Для этого служит класс описания плагина (об этом чуть позже), функция LibClassDesc, в зависимости от номера, передаваемого в качестве параметра, возвращает адрес того или иного класса описания. Типичная реализация:

__declspec(dllexport) ClassDesc *LibClassDesc(int i)
{
  switch (i)
  {
  case 0: return &MyPlugCD;
  default: return 0;
  }
}

LibDescription().

Представьте такую ситуацию: вы использовали какой-то плагин, сохранили свою работу и дали другу, у которого этого плагина нет. Как быть? Естественно без плагина результат, который получился у вас, у друга не будет. Так вот, функция LibDescription возвращает строку, которая копируется в сейв-файл и отображается, в случае, когда при загрузке нужный плагин не найден. Для плагинов экспорта такая ситуация не представляет угрозы, но стандарт есть стандарт, так что вот вам типичная реализация:

__declspec(dllexport) const TCHAR *LibDescription()
{
  return _T("My first export file plugin");
}

LibVersion().

Контроль версий. Возвращаем версию МАХ'а, под которую скомпилирован плагин. С другими версиями работать не будет.

__declspec(dllexport) ULONG LibVersion()
{
  return VERSION_3DSMAX;
}

С экспортируемыми функциями все. Осталось сделать def файл, для того чтобы МАХ увидел экспортируемые нами функции.

Теперь давайте посмотрим на класс описания плагина. Так как, наша dll реализует всего один плагин, то, соответственно, имеем всего один класс описания. Сначала код:

class MyPlugClassDesc : public ClassDesc  
{
public:
  int      IsPublic() { return 1; }
  void*      Create(BOOL Loading = FALSE) { return new MyExp; }
  const TCHAR  *  ClassName() { return _T("MyExp"); }
  SClass_ID    SuperClassID() { return SCENE_EXPORT_CLASS_ID; }
  Class_ID    ClassID() { return MYEXP_CLASS_ID; }
  const TCHAR  *  Category() { return _T(""); }
};

Как можно заметить, дескриптор класса  должен быть порождён от базового класса ClassDesc. Это абстрактный класс, и для возможности создания экземпляра от его наследника (в нашем случае MyPlugClassDesc), необходимо переопределить все чисто-виртуальные функции. Начнем по порядку.
int IsPublic(). Возвращает FALSE, если пользователь 3D Studio не должен иметь возможности использовать этот плагин, и TRUE если такая возможность присутствует. Нужно для случая, когда один плагин использует другой (приватный), и этот приватный плагин не предназначен для взаимодействия с пользователем.

void *Create(BOOL Loading = FALSE). МАХ вызывает эту функцию, когда ему требуется указатель на новый экземпляр класса плагина.

const TCHAR* ClassName(). Возвращает имя класса. Это имя будет высвечено на кнопке плагина в интерфейсе МАХ'а. Так как, экспортерам не соответствуют какие-либо кнопки, то данной надписи мы не увидим.

SClass_ID SuperClassID(),Class_ID ClassID(). А теперь очень важный момент. Каждый плагин должен иметь уникальный (!) идентификатор, который состоит из двух 32 битных квалификаторов. Для каждого плагина эти номера должны быть уникальные, иначе возникнет конфликт при загрузке, и плагин работать не будет. Учтите это также, когда будете брать как примеры для своих собственных наработок код из примеров поставляемых с МAX SDK. Для генерации уникального идентификатора discreet поставляет утилитку под названием gencid.exe, которая расположена в папке maxsdk\help\. Запустите ее, сгенерируйте новый Class_ID, и вставьте в программу в виде макроса:

#define MYEXP_CLASS_ID Class_ID(0x280d4a49, 0x4abe3694)

Вернемся к нашим функциям. ClassID возвращает уникальный Class_ID нашего плагина, а SuperClassID возвращает ID соответствующий типу созданного нами плагина. Для экспортеров это будет SCENE_EXPORT_CLASS_ID.

const TCHAR* Category() определяет принадлежность нашего плагина к какой-либо категории на командной панели ("Standard Primitives", "Particle Systems", "NURBS Surfaces", вы все их видели). Имя заданное этой функцией, определяет в какую категорию попадет плагин. Осторожно, МАХ разрешает использовать не более 12 плагинов в одной категории, так что, если будете писать какой-то визуализационный плагин, то discreet настоятельно рекомендует создавать для него собственную категорию, а не помещать в уже существующую. Наш плагин вообще в категории не нуждается, так что возвращаем пустую строку.

Реализация экспорта

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

Итак, все классы экспорта сцены должны быть порождены от класса SceneExport. Смотрите код:

class MyExp : public SceneExport  
{
public:
  int        ExtCount()      { return 1; }
  const TCHAR*  Ext(int i)      { if (i == 0) return _T("MMM"); else return _T(""); }
  const TCHAR*  LongDesc()      { return _T("My export first plugin"); }
  const TCHAR*  ShortDesc()      { return _T("My exporter"); }
  const TCHAR*  AuthorName()    { return _T("Roman Marchenko"); }
  const TCHAR*  CopyrightMessage()  { return _T("Copyleft (C) 2003"); }
  const TCHAR*  OtherMessage1()    { return _T(""); }
  const TCHAR*  OtherMessage2()    { return _T(""); }
  unsigned int  Version()      { return 100; }
  void      ShowAbout(HWND hWnd){ MessageBox(hWnd,
"First exporter plugin", "About", MB_OK); }
  BOOL      SupportsOptions(int ext, DWORD options);

  // Это функция и производит экспорт.
  int        DoExport(const TCHAR *name, ExpInterface *ei,
               Interface *i, BOOL suppressPromts = FALSE, DWORD options = 0);
  MyExp();
  virtual ~MyExp();
};

Как видите, функций много (все они должны быть определены, так как в классе SceneExport являются чисто-виртуальными), но всего одна служит для основной цели - экспорта. Остальные выполняют сервисные функции для правильного функционирования плагина. Давайте по порядку.

int  ExtCount (). Возвращает количество расширений файлов поддерживаемых плагином.
const TCHAR* Ext(i). В зависимости от передаваемого номера возвращает собственно расширение.
const TCHAR* LongDesc(). Длинное описание экспортируемого файла.
const TCHAR* ShortDesc(). Короткое описание экспортируемого файла.
const TCHAR*  AuthorName(). Имя автора плагина.
const TCHAR*  CopyrightMessage(). Тут ясно.
const TCHAR*  OtherMessage*(). Другие сообщения.
unsigned int Version(). Версия плагина умноженная на 100.
void ShowAbout(HWND hWnd). Информация про плагин.
BOOL SupportsOptions(int ext, DWORD options). Эту функцию можно не определять вообще. Она описывает поддерживаемые опции плагином. Наш, учебный, плагин не будет поддерживать никаких опций, так что simply return FALSE.

int DoExport(const TCHAR *name, ExpInterface *ei,

            Interface *i, BOOL suppressPromts = FALSE, DWORD options = 0);

А вот и главная функция. Она вызывается, когда происходит, собственно, экспорт. В качестве параметров МАХ передает: name - имя сохраняемого файла, ei - интерфейс доступа к геометрии сцены, i - интерфейс через который мы можем вызывать функции, предоставляемые 3d студией разработчику. suppressPromts - если TRUE плагин не должен выводить никаких диалоговых окон требующих ввода пользовательских данных. Такая функциональность требуется для выполнения действий в режиме. options - передаваемые опции. Пока поддерживается только одна: SCENE_EXPORT_SELECTED - экспорт не всей сцены, а только выделенных элементов.

Архитектура сцены и экспорт

А теперь немного теории об архитектуре сцены. Каждый объект, отображенный на сцене 3D studio MAX'а, программно представляет собой геометрический конвейер, в начале которого находится базовый геометрический объект, а на выходе, после применения всех модификаторов, получаем деформированный объект. Для каждого объекта существует ссылка на его конвейер, называемая узел (node). С помощью этой ссылки мо можем получать всю необходимую информацию об объекте: проассоциированный материал, результирующую модель после применения всех модификаторов к базовому объекту, а из модели получим такие параметры как, координаты всех вершин, текстурные координаты, нормали, и т.д. Естественно так же хорошо как в SDK я не напишу, по этому кто заинтересовался, прочитайте раздел Geometry Pipeline System в MAX SDK. Итак, как несложно догадаться, для экспорта сцены нам надо пройтись по всем объектам, проверить их принадлежность к геометрическим телам (источники света, dummy objects, булевы, нам рассматривать нечего) и сохранить параметры в файл.

Также нам понадобится информация о том как хранится в МАХ'е геометрия и текстурные координаты. Сначала с помощью node мы выделим из всего конвейера только структуру, содержащую исключительно геометрическую информацию. Эта структура называется TriObject. Все данные в этой структуре хранятся в виде массивов. Основными (и мы будем ими пользоваться) являются: массив геометрических вершин (координаты каждого вертекса в пространстве), массив текстурных вершин (текстурные координаты) и массив нормалей (нормаль к каждой вершине). "А как же рисовать полигоны?", спросит пытливый читатель. Для этого существует массив полигонов, каждый элемент которого,  содержит 3 номера - индексы вершин в массиве геометрических вершин. С текстурными координатами дело обстоит точно также, только они берутся из массива текстурных вершин. Почему не хранить сразу тройки вершин представляющие полигон? Представьте квадрат 4х4.


В нем 5х5=25 вершин и 4х4х2=32 треугольных полигона. Если хранить способом "по три вершины на полигон" то имеем 32х3x3=288 единиц информации для хранения этого объекта (не забываем что у каждой вершины 3 координаты), а если же методом "массива вершин" то: 25*3 + 32*3 = 171 единиц. Результат налицо. Причем при увеличении количества вершин, а соответственно и полигонов, разница размеров будет расти. Естественно, количество текстурных полигонов и количество геометрических одинаково.

Давайте определимся с тем, что же мы будем экспортировать, и рассмотрим структуру файла. Да, чуть не забыл, наш формат будет бинарный, немного похожий на 3DS. Плагин будет экспортировать базовые свойства материалов: diffuse component, ambient component, specular component, а также shininess. Затем будет следовать имя diffuse текстуры, если таковая присутствует. Также экспортируем геометрические координаты (а как же без них), текстурные координаты и нормали. Для начала хватит. Формат при желании можно будет расширить.

Итак, структура файла:

Троеточие означает, что все повторяется с начала последовательности.

Как видно, файл состоит из объектов, внутри которых сосредоточены: блок информации о материале, блок геометрических и текстурных координат, и блок описания полигонов. Каждый блок начинается с уникального 2-х байтного идентификатора, после которого следует 4-х байтный указатель на следующий блок (в случае с идентификатором узла - после идет указатель на следующий узел). В заголовке узла присутствует: количество вершин, количество текстурных вершин и количество полигонов модели. Все остальное, я надеюсь, понятно из иллюстрации. Цифры означают размер блока в байтах.

А теперь код.

Как мы уже знаем, весь экспорт производится в процедуре DoExport() класса MyExp. Взглянем на реализацию:

int MyExp::DoExport(const TCHAR *name, ExpInterface *ei,
          Interface *i, BOOL suppressPromts, DWORD options)
{
  fFile.open(name, ios::out | ios::binary);
  // Глобализуем переменную i
  ip = i;
  // Проходим по всем узлам сцены.
  ei->theScene->EnumTree(&MyTreeEnum);

  fFile.close();
  return 1;
}

Все просто. Сначала открываем файл. Затем глобализуем переменную i. Это надо для того, чтобы другие функции, а не только DoExport(), могли пользоваться предоставляемым интерфейсом. А вот 3-я строка очень интересна. Функция EnumTree() вызывается для каждого узла сцены. В качестве параметра мы ей передаем адрес экземпляра класса, порожденного от ITreeEnumProc, который и производит, собственно, обработку узла. Взглянем ближе на упомянутый класс.

class SceneSaver: public ITreeEnumProc
{
public:
  void ExportFaces(TriObject *TObj);
  void ExportVerts(TriObject *TObj, Matrix3 tm);
  void ExportMaterial(INode *node);

  // Главные функции
  int callback(INode *node);
  void ProcNode(INode *node);
};

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

int SceneSaver::callback(INode *node)
{
  ProcNode(node);
  return TREE_CONTINUE;
}

Возвращаемое значение сигнализирует о том, что класс готов принять следующий узел к обработке. ProcNode() экспортирует узел в файл. Смотрите код:

void SceneSaver::ProcNode(INode *node)
{
  int      numF, numV, numTV, CurrHeader, zero = 0, debug = 0xAA, Del;
  streampos  NextNode, VertexPointer, FacePointer, TmpPos;
  // Получаем TriObject из узла
  TriObject *TObj;
  TObj = GetTriObjFromNode(node, Del);
  if (!TObj) return;
  numF = TObj->mesh.numFaces;
  numV = TObj->mesh.numVerts;
  numTV = TObj->mesh.numTVerts;
  CurrHeader = ID_NODE_HEADER;
  // Пишем идентификатор узла и количество вершин и полигонов
  fFile.write((char *)&CurrHeader, 2);
  fFile.write((char *)&numV, sizeof(int));
  fFile.write((char *)&numTV, sizeof(int));
  fFile.write((char *)&numF, sizeof(int));
  // Мы не знаем общую длину записи для этого узла
  // Сохраняем позицию для будущего использования и заполняем
  // ее временно нулями
  NextNode = fFile.tellp();
  fFile.write((char *)&zero, 4);
  // Пишем идентификатор секции материалов.
  CurrHeader = ID_MTL_HEADER;
  fFile.write((char *)&CurrHeader, 2);
  // Запоминаем позицию для будущей записи секции геом. координат
  VertexPointer = fFile.tellp();
  fFile.write((char *)&zero, 4);
  // Экспортируем материал.
  ExportMaterial(node);
  // Записываем текущую позицию как начало секции геом. координат.
  TmpPos = fFile.tellp();
  fFile.seekp(VertexPointer);
  fFile.write((char *)&TmpPos, 4);
  fFile.seekp(TmpPos);
  // Начало секции вершин
  // Пишем заголовок вершинных координат
  CurrHeader = ID_VERTEX_HEADER;
  fFile.write((char *)&CurrHeader, 2);
  // Сохраняем позицию.
  FacePointer = fFile.tellp();
  fFile.write((char *)&zero, 4);
  // Пробегаем по всем вершинам и записываем в файл данные:
  // геом. координаты, текстурные координаты  и нормали,
  // но сначала получим матрице трансформации для узла.
  ExportVerts(TObj, node->GetObjTMAfterWSM(ip->GetTime()));
  // Записываем текущую позицию как начало блока полигонов.
  TmpPos = fFile.tellp();
  fFile.seekp(FacePointer);
  fFile.write((char *)&TmpPos, 4);
  fFile.seekp(TmpPos);
  // Экспортируем данные о полигонах.
  // Сначала заголовок
  CurrHeader = ID_FACE_HEADER;
  fFile.write((char *)&CurrHeader, 2);
  // Теперь данные
  ExportFaces(TObj);
  // Записываем текущую позицию, как начало нового узла.
  TmpPos = fFile.tellp();
  fFile.seekp(NextNode);
  fFile.write((char *)&TmpPos, 4);
  fFile.seekp(TmpPos);
}

В коде я намеренно старался делать много комментариев, чтобы все было понятно. Стоит обратить внимание на несколько моментов. Функция GetTriObjectFromNode() возвращает класс TriObject, о котором я упоминал выше. Все геометрические объекты на сцене должны уметь возвращать TriObject из node. Если объект другого типа (например источник света, или типа Boolean), то GetTriObjectFromNode вернет NULL. В реализации ничего сложного нет:

TriObject *GetTriObjFromNode(INode *node, int &deleteIt)
{
  deleteIt = FALSE;
  Object *obj = node->EvalWorldState(ip->GetTime()).obj;
  if (obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))
  {
    TriObject *tri = (TriObject *) obj->ConvertToType(ip->GetTime(), 
                              Class_ID(TRIOBJ_CLASS_ID, 0));
    if (obj != tri) deleteIt = TRUE;
    return tri;
  }
  else return NULL;
}

Самая важная строка: Object *obj = node->EvalWorldState(ip->GetTime()).obj; Функция EvalWorldState возвращает результат вычисления всего конвейера узла, применяя к базовому объекту все модификаторы, преобразования и т.д.

Вернемся к функции ProcNode(). Следующее, на что хотелось бы обратить внимание это то, что все подблоки, материалов, вершин и полигонов, экспортируются в функциях ExportMaterial(), ExportVerts() и ExportFaces() соответственно.

void SceneSaver::ExportMaterial(INode *node)
{
  Color    Cl;
  float    Shininess;
  char    zero = 0;
  // Получаем материал ассоциированный с узлом
  Mtl *m = node->GetMtl();
  if (!m) return;
  // Проверяем стандартный ли это материал.
  if (m->ClassID() != Class_ID(DMTL_CLASS_ID, 0)) return;
  StdMat *mStd = (StdMat *)m;
  // Получаем Ambient компоненту
  Cl = mStd->GetAmbient(ip->GetTime());
  fFile.write((char *)&Cl.r, sizeof(float));
  fFile.write((char *)&Cl.g, sizeof(float));
  fFile.write((char *)&Cl.b, sizeof(float));
  // Получаем Diffuse компоненту
  //...
  // Получаем Specular компоненту
  //...
  // Получаем shininess
  //...
  // Получаем Diffuse имя файла текстуры если таковая есть
  Texmap *tmap = m->GetSubTexmap(ID_DI);
  if (!tmap) 
  {
    fFile.write(&zero, 1);
    return;
  }
  if (tmap->ClassID() != Class_ID(BMTEX_CLASS_ID, 0)) return;
  // Если это битмап
  BitmapTex *bmt = (BitmapTex *)tmap;
  // Пишем имя файла.
  fFile << ExtractFileName(bmt->GetMapName()).data();
  fFile.write(&zero, 1);
}

В экспорте материала ничего сложного нет. Алгоритм таков: получаем материал узла, проверяем его на принадлежность к типу . Потом сохраняем в файл компоненты (в вышеприведенном листинге некоторые моменты пропущены для экономии места). Потом получаем подтекстуру диффузного компонента вызовом GetSubTexmap(). Эта функция получает в качестве параметра идентификатор подтекстуры, которую мы хотим получить. ID_DI - Diffuse, ID_AM - Ambient, ID_SP - Specular, и т.д. Всех идентификаторов достаточно много, полный перечень можно посмотреть в SDK в разделе . Не забывайте, что мы экспортируем только имя файла, по этому вы должны сами позаботиться о присутствии текстуры там, где ваша игра сможет ее найти. Сама текстура не входит в файл экспорта!

Экспорт данных о вершинах:

void SceneSaver::ExportVerts(TriObject *TObj, Matrix3 tm)
{
  int i, Hdr;
  Point3  nCoord;
  DWORD zero = 0;

  // Просчитываем нормали
  TObj->mesh.buildNormals();
  // Проходим по всем вертексам модели
  // и пишем все геометрические координаты вершин и нормали.
  for (i = 0; i < TObj->mesh.numVerts; i++)
  {
    Point3 v = tm * TObj->mesh.verts[i];
    fFile.write((char *)&v.x, sizeof(DWORD));
    fFile.write((char *)&v.z, sizeof(DWORD));
    fFile.write((char *)&v.y, sizeof(DWORD));
    // Пишем нормали (x, y, z)
    nCoord = TObj->mesh.getNormal(i);
    fFile.write((char *)&nCoord.x, sizeof(float));
    fFile.write((char *)&nCoord.z, sizeof(float));
    fFile.write((char *)&nCoord.y, sizeof(float));
  }
  // Пишем текстурные координаты если таковые есть.
  if (TObj->mesh.numTVerts != 0)
  {
    // Записываем заголовок секции
    Hdr = ID_TVERTEX_HEADER;
    fFile.write((char *)&Hdr, 2);
    // Проходим в цикле по всем текстурным вершинам
    for (i = 0; i < TObj->mesh.numTVerts; i++)
    {
      fFile.write((char *)&TObj->mesh.tVerts[i].x, sizeof(DWORD));
      fFile.write((char *)&TObj->mesh.tVerts[i].y, sizeof(DWORD));
    }
  }
}

Экспорт вершин тоже довольно прост. Функция получает на входе TriObject который содержит информацию о координатах, и матрицу трансформации, на которую надо умножить все вершины, чтобы получить их реальное положение в пространстве. Если умножение не сделать, то все координаты будут в локальном пространстве объекта. У вас модель состоит из одного геометрического узла? Тогда нет проблем, можете не умножать. Если из нескольких, расположенных в определенном порядке относительно друг друга, и вы хотите сохранить это положение, то - необходимо. Попробуйте убрать умножение и  посмотрите, что получится. Так же, я поменял порядок следования координат. Для привычной интерпретации в OpenGL (x-вправо, y-вверх, z-к нам) надо координаты z и y поменять местами.

Дальше идут текстурные координаты. Важно запомнить, что они не будут генерироваться, если это явно не сказано 3D МАХ'у. Делается это выставлением галочки Generate Mapping Coordinates в параметрах геометрического объекта, или применением любого текстурного модификатора, например UVW Map. Заметьте также, что мы не экспортируем w координату текстуры.
Экспорт полигонов вообще элементарен. Вся функция в 5 строк:

void SceneSaver::ExportFaces(TriObject *TObj)
{
  int    i;
  // Пробегаемся по всем полигонам и записываем данные в файл
  for (i = 0; i < TObj->mesh.numFaces; i++)
  {
    fFile.write((char *)&TObj->mesh.faces[i].v[0], sizeof(DWORD) * 3);
    if (TObj->mesh.numTVerts != 0)
      fFile.write((char *)&TObj->mesh.tvFace[i].t[0], sizeof(DWORD) * 3);
  }
}

Вот и все.

А зачем нам экспортер, если нет лоадера? Есть. Я его рассматривать не буду, так как всю информацию, для написания собственного, я вам дал, да и в сопроводительном примере к статье есть реализация загрузки наших моделей. Посмотрите код, если что-то будет непонятно. Ах да, использовать так: запускаем МАХ (в папке stdplugs уже должна лежать наша скомпилированная библиотека), создаём 3D модель, нажимаем file->export, там выбираем наш тип файла (My exporter, *.MMM) и сохраняем.

Последние напутствия.

Хочется, напоследок, сказать несколько напутствующих слов.

Во-первых, читайте SDK, там все, о чем я говорил, изложено. Правда, SDK сделан неудобно и, порой, нужную информацию найти непросто.

Во-вторых, с пятой версией МАХ SDK идет такая полезнейшая вещь, как Public Sparks Message Archive. Это архив форумов разработчиков плагинов, где очень много полезного материала, и практически все неясные моменты можно выяснить, внимательно поискав в этом архиве. Файл называется sparks_archive.chm, и находится он в папке maxsdk/help/

Вроде бы, все. Желаю удачи в освоении нового дела. Если что-то непонятно, есть замечания или критика, не стесняйтесь, пишите: vortex @ library.ntu-kpi.kiev.ua, всегда рад вашим письмам.

Пример к статье: 20030917.zip

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

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
Аноним
+1 / -0
Мне нравитсяМне не нравится
29 марта 2006, 17:19:18
2 avn, пример работает, есил убрать комментарий с строчки где подключается comctl32.lib
2.
Аноним
Мне нравитсяМне не нравится
27 декабря 2005, 20:05:08
Статья ничего, но пример не работает :(
3.
Аноним
Мне нравитсяМне не нравится
20 декабря 2005, 18:18:10
Вот попал на тему, давно искал :)) видимо не там. Благодарю.
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог