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

Создание Export плагина для 3D Studio MAX. (2 стр)

Автор:

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

А теперь немного теории об архитектуре сцены. Каждый объект, отображенный на сцене 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 в разделе «List of Texture Map Indices». Не забывайте, что мы экспортируем только имя файла, по этому вы должны сами позаботиться о присутствии текстуры там, где ваша игра сможет ее найти. Сама текстура не входит в файл экспорта!

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

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.

Страницы: 1 2

#3D Studio MAX, #плагины, #экспорт

18 сентября 2003 (Обновление: 15 июня 2009)

Комментарии [87]