Войти
Уголок tool-программСтатьи

3DS MAX в качестве редактора 64K-интро

Автор:

Вступление
Требования
Примитив обыкновенный
Мир клонов
Модификаторы
«Смотри, ма, я умею работать с иерархией!»
Секретные материалы
Камера, свет, мотор!
“Animate!” (из старинной 4к интры)
«Эта ошибка никогда не должна была случиться» (из виденного в винде)
Подводим итоги
Что дальше?
Ссылки:

Вступление

Даная статья была первоначально создана для демосценерского журнала HUGI. Основное внимание акцентировалось на вопросах использования 3D Studio для создания интро размером 64 килобайта (т.е. одним из наиболее важных решаемых вопросов помимо самого экспорта была оптимизация извлекаемых данных по размеру). Тем не менее, надеюсь, что приведенная ниже информация окажется полезной и для какого-то числа геймдевелоперов, собирающихся использовать или уже использующих «макс» в своём проекте.

Те из вас, кому доводилось писать хоть какой-нибудь редактор (которым в состоянии пользоваться не только вы (да ещё и без «пол-литры»)), должны быть в курсе сложности этой задачи. Приходится тратить кучу времени на вопросы напрямую не связанные с демой/игрой, а о скучности некоторых этапов вообще умолчу. Одним из возможных способов обхода этих проблем является использование имеющихся программ.. Большинство существующих коммерческих приложений расширяемы за счёт использования внешних плагинов. Создание плагина экспорта/импорта может оказаться довольно гибким решением. В данной статье я постараюсь описать наиболее важные моменты создания подобного плагина для небезызвестного 3Д МАКСа с учетом специфики 64к интр.
Предполагается, что вы знакомы с основами плагиностроения, у вас установлен MAX SDK(в противном случае читайте вводные статьи на данном сайте и не брезгуйте ссылками в конце статьи), вы обуздали основы машинной графики и линейной алгебры (на уровне понимания таких терминов как кватернион и матрица). Очень полезно представлять, что из себя представляет геометрический конвейер MAX-а (хотя бы посмотрите картинки Geometry Pipeline System в MAX SDK). Кстати, я использую 3D MAX 4/5, так что не ругайтесь, если кое-какая информация успела устареть.

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

Требования

Попытаемся сформулировать, чего же мы хотим получить от МАКСа:

1.  Модели
  1.1.  Тип примитива
      1.1.1.  Сам примитив и все его параметры (напр., радиус, число сегментов, сглаживание)
      1.1.2.  Клоны (напр., информация о том, что 10 боксов были получены клонированием из 1)
  1.2.  Мировые координты, ориентация в пространстве (углы Эйлера, кватернион), масштабные коэффиценты.
  1.3.  Список модификаторов (трапеция, изгиб и пр.) с учётом клонирования
  1.4.  Иерархические связи модели (напр., колёса привязанные к платформе машины)
  1.5.  Материалы
      1.5.1.  Название картин(ки,ок)
      1.5.2.  Параметры текстуры (тайлинг, альфа, световые параметры)
2.  Камеры
  2.1.  Параметры (телесный угол, плоскости отсечения…)
  2.2.  Мировые параметры (ориентация, координаты)
3.  Источники света
  3.1.  Параметры (цвет, тип…)
  3.2.  Мировые параметры (ориентация, координаты)
4.  Анимации
  4.1.  Объектов (перемещение, вращение, масштабирование)
  4.2.  Камер (перемещение, вращение)
  4.3.  Источников света (перемещение, вращение)

Кому-то этого много, кому-то мало. В общем, перейдем к сути.

Примитив обыкновенный

Предположим, вы достучались до текущего узла (INode) в своей callback процедуре. Вот некоторые полезные методы INode-а:
TSTR GetName() – Имя узла. Крайне удобно использовать эту функцию при отладке (вряд ли вы захотите таскать за собой и имя внутри интры). Кстати, давать всем узлам сцены (или хотя бы тем, которые активно клонируются) внятные имена крайне полезная привычка – не ленитесь.

ULONG GetHandle() – Возвращает идентификатор 3DMAX-а для текущего узла.

Object* GetObjectRef() – Возвращает указатель на объект Object, на который ссылается данный узел (более подробно ниже).

Функции Object-а (некоторые методы просто получены от базовых классов, но это сейчас не важно):

ClassID ClassID() - Возвращает ClassID объекта, которое может быть использовано для определения типа примитива (бокс, сфера).

SClassID SuperClassID() – Возвращает SClass_ID текущего объекта. Крайне полезный метод, по той причине, что некоторые объекты наследованы от базовых классов. Их базовый тип можно определить как раз с помощью данной функции.  Хотя следующий метод более полезен.

int CanConvertToType(Class_ID) - Возвращает не 0, если текущий объект может быть сконвертирован в некий переданный тип.

Пример:

if (obj->CanConvertToType(Class_ID(BOXOBJ_CLASS_ID,0)))say(“Вах, это бокс!”);

Мне больше по душе создать функцию, которая получает объект, а возвращает идентификатор примитива (под который отводится 1 байт) или код ошибки, если примитив не поддерживается.
Если к объекту был применён модификатор (bend, taper) или его клонировали (подробнее ниже), его SClassID будет чем-то вроде GEN_DERIVOB_CLASS_ID. Узнать настоящий тип примитива (показываемый в самом низу стека модификаторов МАКСа)  можно с помощью:

Object *FindBaseObject() – Возвращает базовый объект в стеке модификаторов (т.е. объект до модификации). А тип возвращаемого объекта можно определить с помощью некогда упомянутой CanConvertToType(Class_ID), которая для базового объекта вернёт ожидаемый «правильный» тип.

После определения типа объекта необходимо получить его параметры. МАКС позволяет использовать гибкий подход, основанный на обобщенных параметрах. У каждого объекта (также верно для модификаторов и силовых полей) имеется набор параметров, значение каждого их которых можно получить. Вот главные функции (определенные как виртуальные в классе BaseObject – одном из базовых классов Object-а):

IParamArray *GetParamBlock() – Возвращает указатель на объект, хранящий массив всех параметров объекта (каждый их которых может быть различного типа).

Наиболее значимая функция IParamArray-я:

BOOL GetValue(int i, TimeValue t, SOME_TYPE &v, Interval &ivalid) –  возвращает значение параметра с индексом i в момент времени t. SOME_TYPE может быть одним из следующих типов: float, int или Point3; после вызова функции в v будет значение нужного параметра. В случае успешного завершения функция вернёт TRUE.

int GetParamBlockIndex(int id) – Возвращает индекс параметра id. Эта функция используется для получения индекса нужного параметра в массиве (можно взять их и из MAXSDK\Include\istdplug.h, но вы понимаете, что в новой версии эти макросы могут измениться).

Пример:

// сфера?
if (object->CanConvertToType(Class_ID(SPHERE_CLASS_ID,0)))
{
   // получить указатель на текущий массив параметров:
   IParamArray* params = object->GetParamBlock();
   // настоятельно рекомендую ПОСТОЯННО проверять возвращаемые значения. 
   // МАКС очень легко повесить и очень тяжко ждать его перезагрузки:
   if(!params)alertExit(“NULL params!!!”);
   
   // получить индексы базовых параметров:
   int radius_i  =  object->GetParamBlockIndex(SPHERE_RADIUS);
   int smooth_i  =  obj->GetParamBlockIndex(SPHERE_SMOOTH);
   int segs_i    =  obj->GetParamBlockIndex(SPHERE_SEGS);
   int hemi_i    =  obj->GetParamBlockIndex(SPHERE_HEMI);
   …
   // в этих переменных будут храниться значения переменных:
   float  radius,hemi;
   int  segments,smooth;
   …
   // получить значения:
   params->GetValue(radius_i,0,radius,interval);
   params->GetValue(smoth_i,0,smooth,interval);
   params->GetValue(segs_i,0,segments,interval);
   params->GetValue(hemi_i,0,hemi,interval);
…
}// if (object->CanConvertTo(Class_ID(SPHERE_CLASS_ID,0)))

NB Я недаром обратил ваше внимание на проверку возвращаемых значений. Конечно, мы все привыкли лениться и не заморачиваться с вопросами следования указаниям документации, полагая, что все стандартные функции API не приводят к проблемам. Однако при работе с МАКСом надо постоянно держать в голове принципы оборонного программирования (вкратце: не делайте опасных предположений). Потому как, при любой малейшей ошибке вероятность краша МАКСа крайне велика (например, при попытке получить доступ к параметрам UV-модификатора всегда возвращается пустой указатель). Времени на перезапуск 3Д Сутдии понадобится куда больше, чем на добавление пары «лишних» строк кода.

Теперь вернемся к методам INode:

Matrix3 GetNodeTM(TimeValue t, Interval* valid=NULL) – Возвращает матрицу узла размерности 4х3 (перенос в последней строке) в указанный момент времени. Именно из этой матрицы мы будем получать данные о мировых координатах объектов (о декомпозиции речь пойдет позже).

Вот некоторые из наиболее полезных методов Matrix3:

Point3 GetTrans() – Возвращает компоненту переноса.

Parity() – Возвращает true, если одна из осей имеет отрицательный масштаб (например, после зеркального отображения). Нужно осуществить проверку на четность для определения необходимости инвертирования нормалей (в случае, когда происходит экспорт модели с нормалями) или изменить значения углов Эйлера.

Invert() – инвертирование матрицы (ух ты!).

GetYawPitchRoll(float *yaw, float *pitch, float *roll) – возвращает углы Эйлера на основе информации о компонентах вращения матрицы. Функция может вернуть NaN (не число) из-за ошибок округления. Следующая функция решает эту проблему:

// by Lanier David
void fixedGetYawPitchRoll(Matrix3* m, float *Yaw, float *Pitch, float *Roll) 
{
   if(!m || !Yaw || !Pitch || !Roll) return;

   const Point4& Col0 = m->GetColumn(0);
   const Point4& Col1 = m->GetColumn(1);
   const Point4& Col2 = m->GetColumn(2);

   double sh, ch, sr, cr;
   double sp = -Col1[2];
   double temp = 1.0 - sp * sp;
   if (temp < 0.0) temp = 0.0; //нормировка для избежания проблем
   double cp = sqrt(temp);
   if (cp < 1E-6) 
   {
      sh = -Col2[0];
      ch = Col0[0];
      sr = 0.0;
      cr = 1.0;
   }
   else 
   {
      sh = Col0[2] / cp;
      ch = Col2[2] / cp;
      sr = Col1[0] / cp;
      cr = Col1[1] / cp;
   }
   
   *Roll = (float)atan2(sr, cr);
   *Pitch = (float)atan2(sp, cp);
   *Yaw = (float)atan2(sh, ch);
}
Страницы: 1 2 3 4 Следующая »

11 июня 2006 (Обновление: 21 июля 2006)