netlib.narod.ru | < Назад | Оглавление | Далее > |
Для вашего удовольствия я включил в сопроводительные файлы к книге очень простой редактор анимации. Как это ни странно, проект называется D3D_AnimationEditor. Пожалуйста, загрузите его, и давайте проследуем дальше.
Программа D3D_AnimationEditor представляет собой простейший редактор анимации, позволяющий вам анимировать два объекта сцены. Первый объект — это механоид, а второй — небольшая антена радара. Вы можете добавлять ключевые кадры, вращать объекты, перемещать их по сцене и даже перемещать камеру, чтобы взглянуть на сцену с разных точек. Программа также позволяет сохранять созданные анимации и затем загружать их.
Чтобы получить представление о программе, взгляните на рис. 11.17, где изображено ее окно, или просто запустите приложение.
Рис. 11.17. Окно программы D3D_AnimationEditor
На рис. 11.7 изображено как реализация редактора анимации выглядит в реальной жизни. Вы, возможно, скажете, что она очень похожа на интерфейс редактора, созданного в предыдущей главе. Окно слева содержит область редактирования с отладочной информацией, а окно справа — панель команд и информацию. В окне редактирования вы видите загруженные и готовые к использованию объекты механоида и антены радара. Если вы скомпилировали и запустили программу, чтобы она стала похожа на рис. 11.17, щелкните по кнопке Load Anim.
Если вы еще не сделали этого, запустите редактор анимации и щелкните по кнопке Load Anim. В результате будет загружена предварительно созданная мной анимация. После выполнения загрузки щелкните по кнопке Start/Stop Anim, чтобы начать воспроизведение анимации. Если все прошло хорошо, вы увидите как расположенная на голове механоида радарная антена начнет вращаться.
Вы можете остановить воспроизводимую анимацию, еще раз щелкнув по кнопке Start/Stop Anim. Фактически эта кнопка переключает состояние анимации между воспроизведением и паузой. Пока анимация воспроизводится вы можете даже добавлять в нее ключевые кадры и изменять данные. Это подводит меня к следующему набору элементов управления — элементы управления кадрами. На панели команд есть три команды для работы с кадрами: Prev, Next и New. Щелчок по кнопке Prev Frame приводит к переходу от текущего кадра анимации к предшествующему в списке. Если вы достигли начала анимации, последовательность кадров замыкается и вы перейдете к последнему кадру последовательности. Перебирая кадры вы будете видеть их данные, выводимые в области отладочной информации главного окна редактирования. Кроме того, данные местоположения и вращения появляются в текстовых полях, расположенных в окне панели команд.
Три текстовых поля в левом столбце показывают данные о местоположении текущего объекта. Вы можете изменять эти числа, и в результате текущий объект будет перемещен в заданную позицию.
Три текстовых поля в правом столбце задают информацию о вращении текущего объекта. Вы можете изменять эти данные точно так же, как и информацию о местоположении.
Следом на панели инструментов расположены команды для работы с объектами: Prev Obj, Next Obj и Load Objects. Щелчок по кнопке Prev Obj приведет к тому, что текущим станет объект, который был добавлен к сцене перед выбранным в данный момент. Если вы достигли начала списка объектов, текущим станет последний добавленный к сцене объект. Кнопка Next Obj работает точно так же, но перебирает объекты в прямом, а не в обратном направлении. В рассматриваемом примере есть всего лишь два объекта (механоид и антена радара). Кнопка Load Objects используется для загрузки в сцену трехмерных объектов. Чтобы упростить пример я жестко запрограммировал эту кнопку на загрузку механоида и антены радара. В реальном редакторе вам надо будет добавить возможность выбора произвольного объекта для загрузки, а не прописывать все в коде.
И, наконец, кнопки Load Anim и Save Anim. Кнопка Load Anim загружает файл анимации с именем RobotIdle. Он содержит анимацию, специально созданную мной для данного примера. В реальном редакторе вам надо указывать имя загружаемого файла, а в примере для простоты я его жестко прописал в коде. Кнопка Save Anim записывает данные о созданной в редакторе анимации в файл с именем RobotIdle. Будьте очень аккуратны, чтобы по неосторожности не перезаписать файл!
Проект D3D_AnimationEditor сложнее, чем остальные рассматриваемые в этой книге проекты, но не намного. Взгляните на рис. 11.18, где изображены входящие в проект файлы.
Рис. 11.18. Структура файлов проекта D3D_AnimationEditor
На рисунке видно, как в файл main.cpp включается файл main.h. Файл main.h в свою очередь включает заголовочный файл C3DAnimation.h. Заголовочный файл C3DAnimation.h содержит сведения о классе анимации и заголовочный файл Object3DClass.h. Заголовочный файл Object3Dclass.h включает информацию для загрузки объектов из файлов .x и их визуализации. Кроме того, он включает заголовочный файл ExceptionClass.h. Заголовочный файл ExceptionClass.h содержит информацию о классе исключений и заголовочный файл DXUtil.h, предоставляемый DirectX SDK.
Если посмотреть на файлы, которые не являются заголовочными, то основная логика редактора находится в файле main.cpp. Класс анимации расположен в файле C3DAnimation.cpp, класс трехмерных объектов — в файле Object3DClass.cpp, а класс исключений — в файле ExceptionClass.cpp.
Что касается библиотек, то для успешной компиляции проекта нам потребуются: d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib, winmm.lib и dinput8.lib.
Вот и все, что я хотел сказать о структуре проекта редактора анимации. Без дополнительных разглагольствований я представляю вам класс анимации. Барабанная дробь, пожалуйста...
Для программы D3D_AnimationClass я создал класс содержащий всю информацию, необходимую для создания, редактирования, сохранения, загрузки и воспроизведения анимации. Все это может показаться довольно сложным, но поиграйте немного с примером, и окажется, что все не так уж и плохо. Взгляните на рис. 11.19, иллюстрирующий определение класса.
Рис. 11.19. Структура класса C3DAnimation
На рисунке методы класса сгруппированы согласно их назначению. Например, сгруппированы все функции, относящиеся к работе с кадрами. Перед тем, как описывать функции, я приведу код заголовка класса:
const int g_iMaxObjects = 16; const int g_iMaxKeys = 1024; struct stKeyFrame { D3DXVECTOR3 m_vecRot; D3DXVECTOR3 m_vecTrans; D3DXVECTOR3 m_vecScale; long m_lTimeDelay; }; class C3DAnimation { public: stKeyFrame *m_keyFrames[g_iMaxObjects][g_iMaxKeys]; Object3DClass *m_objObject[g_iMaxObjects]; char m_szObjectName[g_iMaxObjects][32]; int m_iNumFrames; int m_iNumObjects; char m_szAnimName[64]; int m_iCurFrame; long m_lCurTime; LPDIRECT3DDEVICE9 m_pd3dDevice; C3DAnimation(); ~C3DAnimation(); void vNewFrame(void); int iNextFrame(void); int iPrevFrame(void); int iStartFrame(void); int iNewObj(char *szObjName); void vUpdateTrans(int iObj, int iKey, D3DXVECTOR3 vecTrans); void vUpdateRot(int iObj, int iKey, D3DXVECTOR3 vecRot); void vUpdateScale(int iObj, int iKey, D3DXVECTOR3 vecScale); void vSave(char *szFileName); void vLoad(char *szFileName); void vSet3DDevice(LPDIRECT3DDEVICE9 pd3dDevice); void vReset(void); };
Методы класса анимации реализуют базовый набор действий, необходимых для редактирования. Ниже приводится их краткое описание.
Функция C3DAnimation() — это стандартный конструктор класса, который инициализирует члены данных класса анимации.
Функция ~C3DAnimation() — это деструктор, освобождающий выделенную память при уничтожении объекта.
Функция vSave() вызывается для записи данных анимации в указанный файл.
Функция vLoad() вызывается для загрузки указанного файла анимации.
Функция vSet3Ddevice() применяется для установки внутреннего указателя на устройство Direct3D.
Функция vReset() освобождает всю выделенную для объекта анимации память и выполняет инициализацию данных. Ее вызывает конструктор во время инициализации объекта.
Функция vNewFrame() создает новый кадр для каждого объекта анимации.
Функция iNextFrame() осуществляет переход к следующему кадру.
Функция iPrevFrame() осуществляет возврат к предыдущему кадру.
Функция iStartFrame() выполняет возврат анимации к начальному кадру.
Функция iNewObj() добавляет трехмерный объект к анимируемой сцене.
Функция vUpdateTrans() получает вектор и прибавляет его к текущему вектору местоположения выбранного объекта в заданном кадре.
Функция vUpdateRot() получает вектор и прибавляет его к текущему вектору вращения выбранного объекта в заданном кадре.
Функция vUpdateScale() получает вектор и прибавляет его к текущему вектору масштабирования выбранного объекта в заданном кадре.
Методы класса не выглядят слишком сложными, но члены данных могут сильно удивить вас. К примеру, член данных для хранения ключевых кадров является многомерным массивом ключевых кадров. Размер массива определяется количеством объектов в сцене и числом кадров в анимации. Это вызвано тем, что для каждого кадра анимации вам необходим ключевой кадр для каждого объекта сцены. Чтобы было доступно достаточное количество ключевых кадров, вы создаете массив заданного размера, как показано в приведенном выше коде. Все это иллюстрирует рис. 11.20.
Рис. 11.20. Массивы в классе C3DAnimation
Обратите внимание, что массив ключевых кадров двухмерный. Первое измерение содержит кадры для каждого объекта в анимации, а второе — кадры для каждого ключа анимации. Массив объектов, с другой стороны, одномерный. Он хранит все трехмерные объекты, участвующие в анимации.
Еще один одномерный массив называется m_szObjectName. Его назначение — хранить имена участвующих в анимации трехмерных объектов. Это требуется для анимации, чтобы знать какие трехмерные объекты загружать когда используется функция vLoad().
Член данных m_iNumFrames применяется для того, чтобы отслеживать количество кадров в анимации. Вам следует помнить, что для каждого ключа должен существовать кадр каждого объекта. Например, у вас есть пять объектов и в вашей анимации 30 ключей — для хранения этой анимации потребуется 150 кадров: количество объектов * количество ключей = количество кадров.
Переменная m_iNumObjects отслеживает количество задействованных в анимации объектов. Для каждого объекта необходимо задать его имя.
В символьном массиве m_szAnimName хранится имя анимации.
Член данных m_iCurFrame отслеживает номер текущего кадра. Этот номер используется при воспроизведении анимации и не сохраняется при ее записи на диск.
Член данных m_lCurTime отслеживает сколько времени прошло с момента последней смены кадра. Благодаря ему можно реализовать различные временные задержки между сменой кадров.
Переменная m_pd3dDevice хранит указатель на активное устройство визуализации Direct3D. Она необходима для загрузки файлов .x, содержащих используемые в анимации объекты.
В начале заголовочного файла расположено объявление структуры stKeyFrame. Это небольшая структура созданная мной для хранения всей информации кадра для каждого ключа анимации. Ниже приведен список членов этой структуры с кратким описанием их назначения.
Член m_vecRot содержит информацию о вращении для ключа.
Член m_vecTrans содержит информацию о перемещении, или местоположении, для ключа.
Член m_vecScale содержит информацию о масштабировании для ключа.
Член m_lTimeDelay содержит информацию о временной задержке для ключа.
Имейте в виду, что главная цель класса анимации — хранение и воспроизведение трехмерных анимаций. Это не значит, что вы ограничены воспроизведением переходных сцен межу уровнями или чем-то подобным. Главная цель класса — хранить анимацию для движения объектов, сражений и других действий, происходящих во время игры. Хорошо, давайте вернемся к нашей постоянно откладываемой программе.
Файл C3DAnimation.cpp содержит код реализации методов класса, и первыми нам на глаза попадаются конструктор и деструктор.
Конструктор вызывается в момент создания нового объекта класса анимации. Вот как выглядит его код:
C3DAnimation::C3DAnimation() { int i, j; // Установка количества объектов m_iNumObjects = 0; // Установка количества кадров m_iNumFrames = 0; // Установка начального состояния анимации m_iCurFrame = 0; m_lCurTime = 0; // Инициализация объектов, имен и информации о ключах for(i = 0; i < g_iMaxObjects; i++) { // Объекты m_objObject[i] = NULL; // Имена strcpy(&m_szObjectName[i][0], ""); // Ключи for(j = 0; j < g_iMaxKeys; j++) { m_keyFrames[i][j] = NULL; } } }
Конструктор начинается с присвоения нулевых значений различным членам данных класса. Обнуляется количество объектов и кадров, номер текущего кадра и счетчик времени. Далее расположены два цикла. Внешний цикл присваивает всем указателям на объекты значение NULL, а внутренний цикл присваивает значение NULL указателям на ключевые кадры объектов. Я делаю это для того, чтобы в дальнейшем правильно работали проверки выделения памяти. Как вы возможно знаете, большинство компиляторов не выполняет автоматическое обнуление неинициализированных данных; поэтому данный этап абсолютно необходим.
Деструктор вызывается когда объект класса анимации удаляется или выходит из области видимости. Вот как выглядит его код:
C3DAnimation::~C3DAnimation() { // Освобождение памяти vReset(); }
Держу пари, вы подумали, что легко отделались? Но на самом деле я лишь отсрочил неизбежное. Деструктор выглядит так просто потому, что он только вызывает функцию vReset(). На этом перейдем к функции сброса значений.
Функция vReset() вызывается для установки содержимого объекта анимации в исходное состояние. Она полезна, когда требуется загрузить новую анимацию поверх существующего объекта. Также она используется при уничтожении объекта анимации. Вот ее код:
void C3DAnimation::vReset(void) { int i, j; // Освобождение объектов for(i = 0; i < m_iNumObjects; i++) { if(m_objObject[i]) { delete m_objObject[i]; m_objObject[i] = NULL; } } // Освобождение данных ключевых кадров for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { if(m_keyFrames[i][j]) { delete m_keyFrames[i][j]; m_keyFrames[i][j] = NULL; } } } // Установка количества объектов m_iNumObjects = 0; // Установка количества кадров m_iNumFrames = 0; // Установка начального состояния анимации m_iCurFrame = 0; m_lCurTime = 0; // Инициализация объектов, имен и информации о ключах for(i = 0; i < g_iMaxObjects; i++) { // Объекты m_objObject[i] = NULL; // Имена strcpy(&m_szObjectName[i][0], ""); // Ключи for(j = 0; j < g_iMaxKeys; j++) { m_keyFrames[i][j] = NULL; } } }
В первой части функции я перебираю объекты сцены и удаляю их, если они существуют. Цикл выполняет работу по освобождению всей памяти выделенной для хранения созданных или загруженных трехмерных объектов.
Следующий цикл перебирает все кадры и удаляет их. Здесь освобождается память, выделенная для новых или загруженных кадров.
Затем обнуляется количество объектов, количество кадров, номер текущего кадра и счетчик времени. Этот код должен быть понятен без дополнительных комментариев.
Заключительный цикл очищает строки с именами трехмерных объектов.
Функция vNewFrame() используется для создания ключевого кадра для каждого объекта сцены. Если создается первый кадр анимации, ему присваиваются ключевые значения по умолчанию. Если кадр не первый, то значения ключей копируются из предыдущего кадра. Благодаря этому упрощается создание анимации, поскольку не надо каждый раз при создании нового кадра заново позиционировать, вращать и масштабировать объект. Вот как выглядит код функции:
void C3DAnimation::vNewFrame(void) { int iFrame = 0; stKeyFrame *ptrFrame; stKeyFrame *ptrPrevFrame; // Увеличение количества кадров в анимации m_iNumFrames++; // Получение индекса ключевого кадра iFrame = m_iNumFrames - 1; // Создаем новый кадр для каждого объекта // в анимации. for(int iObj = 0; iObj < m_iNumObjects; iObj++) { // Выделяем память для кадра m_keyFrames[iObj][iFrame] = new stKeyFrame; // Получаем указатель на новый кадр ptrFrame = m_keyFrames[iObj][iFrame]; // Присваиваем первому кадру значения по умолчанию if(iFrame == 0) { ptrFrame->m_vecScale = D3DXVECTOR3(1.0, 1.0, 1.0); ptrFrame->m_vecRot = D3DXVECTOR3(0.0, 0.0, 0.0); ptrFrame->m_vecTrans = D3DXVECTOR3(0.0, 0.0, 0.0); ptrFrame->m_lTimeDelay = 10; } // Остальным кадрам присваиваем значения, // скопированные из предыдущего кадра else { // Получаем указатель на предыдущий кадр ptrPrevFrame = m_keyFrames[iObj][(iFrame - 1)]; // Копируем данные из предыдущего кадра в текущий ptrFrame->m_vecScale = ptrPrevFrame->m_vecScale; ptrFrame->m_vecRot = ptrPrevFrame->m_vecRot; ptrFrame->m_vecTrans = ptrPrevFrame->m_vecTrans; ptrFrame->m_lTimeDelay = ptrPrevFrame->m_lTimeDelay; } } m_iCurFrame = iFrame; }
В первой части кода увеличивается значение счетчика количества кадров для объекта. Это делается для того, чтобы анимация знала сколько кадров входит в нее.
Затем я инициализирую временную переменную, присваивая ей значение на единицу меньшее количества кадров. В результате значение переменной iFrame будет равно номеру создаваемого кадра.
Затем в цикле перебираются все объекты сцены и выделяется память для нового ключевого кадра для каждого объекта. Затем для созданных кадров устанавливаются значения масштабирования, вращения, перемещения и счетчика времени. Как я уже говорил ранее, эти значения зависят от того, является кадр первым в анимации или нет.
И, наконец, я делаю номер текущего кадра равным значению переменной iFrame. В результате программа перейдет к редактированию только что созданного кадра.
Следующая функция работы с кадрами очень проста, поскольку она всего лишь перемещает указатель текущего кадра на следующий кадр. Если же кадров больше нет, то указатель возвращается назад к самому первому кадру. А вот и код функции:
int C3DAnimation::iNextFrame(void) { // Переход к следующему кадру m_iCurFrame++; // Если кадр был последним, переходим к началу анимации if(m_iCurFrame >= m_iNumFrames) { m_iCurFrame = 0; } // Возвращаем номер кадра return(m_iCurFrame); }
В коде видно как я сначала увеличиваю номер текущего кадра, а затем прверяю существует ли кадр с таким номером в анимационной последовательности. Если номер превышает число кадров в анимации, я возвращаюсь к начальному кадру с номером 0.
Функция перехода к предыдущему кадру работает точно так же, как функция перехода к следующему кадру, но смена кадров осуществляется в обратном направлении. Код уменьшает номер текущего кадра и проверяет не стал ли номер равен –1. Если номер равен –1, то выбирается последний кадр анимации. Вот как выглядит код функции:
int C3DAnimation::iPrevFrame(void) { // Переход к предыдущему кадру m_iCurFrame--; // Если номер кадра меньше нуля, переходим к последнему кадру. // Если кадров нет, переходим к нулевому кадру if(m_iCurFrame < 0) { // Проверяем есть ли кадры if(m_iNumFrames) { // Переход к последнему кадру m_iCurFrame = m_iNumFrames - 1; } // Кадров нет else { // Переход к нулевому кадру m_iCurFrame = 0; } } // Возвращаем номер кадра return(m_iCurFrame); }
В коде вы можете видеть как я уменьшаю значение переменной с номером текущего кадра, чтобы переместиться ближе к началу анимации. Затем я проверяю не стал ли номер кадра меньше 0. Если да, я проверяю есть ли вообще кадры в анимации. Если кадры есть, то текущим становится последний кадр анимации. Если кадров нет, номер текущего кадра остается навным 0. Завершая свою работу функция возвращает номер текущего кадра. Весь этот процесс показан на рис. 11.21.
Рис. 11.21. Выполнение функции перехода к предыдущему кадру
Функция перехода к начальному кадру просто перематывает анимацию до первого кадра. Вот ее код:
int C3DAnimation::iStartFrame(void) { // Переход к первому кадру m_iCurFrame = 0; // Возвращаем номер кадра return(m_iCurFrame); }
Я не буду объяснять самодокументируемый код. Разве вы любите это?
Функция создания нового объекта получает имя файла .x и загружает его в ближайший доступный слот объекта. Вот как выглядит код функции:
int C3DAnimation::iNewObj(char *szObjName) { char szFileName[512]; // Создаем имя файла sprintf(szFileName, "Data\\3DObjects\\%s.x", szObjName); // Устанавливаем указатель на объект m_objObject[m_iNumObjects] = new Object3DClass; m_objObject[m_iNumObjects]->hLoad(szFileName, m_pd3dDevice); // Сохраняем имя для последующего использования strcpy(&m_szObjectName[m_iNumObjects][0], szObjName); // Увеличиваем внутренний счетчик m_iNumObjects++; // Возвращаем количество объектов return(m_iNumObjects); }
В первой части функции создается полностью квалифицированное имя файла. Оно включает путь и имя файла.
В следующем фрагменте кода создается новый объект Object3DClass для хранения данных модели из файла .x. Затем только что созданный объект загружает данные из файла .x с помощью собственной функции hLoad().
Как только данные объекта загружены, код устанавливает имя объекта для дальнейших ссылок. Затем увеличивается счетчик количества объектов и его значение возвращается вызывающей программе.
Помните, что функция создания нового объекта добавляет новый объект ко всей анимации, а не только к одному ключевому кадру.
Функция изменения позиции получает вектор перемещения, номер кадра и номер объекта и добавляет значение перемещения к текущему вектору местоположения заданного объекта в указанном кадре. Ее полезно использовать для изменения местоположения объектов. Вот как выглядит код функции:
void C3DAnimation::vUpdateTrans( int iObj, int iKey, D3DXVECTOR3 vecTrans) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecTrans += vecTrans; } }
Сперва в коде выполняется проверка того, что указанные объект и ключ существуют. Если да, данные вектора изменяются с учетом переданных значений.
Функция изменения поворота получает вектор поворота, номер кадра и номер объекта и и добавляет значение вращения к текущему вектору поворота заданного объекта в указанном кадре. Ее хорошо применять для изменения угла поворота объектов. Вот код функции:
void C3DAnimation::vUpdateRot(int iObj, int iKey, D3DXVECTOR3 vecRot) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecRot += vecRot; } }
Код начинается с проверки того, существуют ли указанные объект и ключ. Если да, данные вектора изменяются с учетом переданных значений.
Функция изменения масштаба получает вектор масштабирования, номер кадра и номер объекта и и добавляет значение масштабирования к текущему вектору масштабирования заданного объекта в указанном кадре. Функция полезна для изменения масштаба объектов. Вот ее код:
void C3DAnimation::vUpdateScale( int iObj, int iKey, D3DXVECTOR3 vecScale) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecScale += vecScale; } }
Код начинается с проверки того, являются ли верными полученные номера объекта и ключа. Если да, данные вектора изменяются с учетом переданных значений.
Ох, парни, начинается разговор о большой функции. Функция записи получает имя файла и сохраняет в нем всю содержащуюся в классе информацию об анимации. Данные включают заголовок, ссылки на объекты и информацию о ключевых кадрах. Вот код функции:
void C3DAnimation::vSave(char *szFileName) { FILE *fp; int i, j; char szFullFileName[512]; // Создаем квалифицированное имя файла sprintf(szFullFileName, "Data\\Anims\\%s.anim", szFileName); // Открываем файл для записи fp = fopen(szFullFileName, "wb"); if(fp == NULL) { return; } // Вывод заголовка // Количество объектов fwrite(&m_iNumObjects, 1, sizeof(int), fp); // Количество кадров fwrite(&m_iNumFrames, 1, sizeof(int), fp); // Вывод имен объектов for(i = 0; i < m_iNumObjects; i++) { fwrite(&m_szObjectName[i][0], 32, sizeof(char), fp); } // Вывод информации о ключевых кадрах for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { // Задержка fwrite(&m_keyFrames[i][j]->m_lTimeDelay, 1, sizeof(long), fp); // Поворот fwrite(&m_keyFrames[i][j]->m_vecRot, 1, sizeof(D3DXVECTOR3), fp); // Масштаб fwrite(&m_keyFrames[i][j]->m_vecScale, 1, sizeof(D3DXVECTOR3), fp); // Местоположение fwrite(&m_keyFrames[i][j]->m_vecTrans, 1, sizeof(D3DXVECTOR3), fp); } } // Закрываем файл анимации fclose(fp); // Сохраняем имя анимации strcpy(m_szAnimName, szFileName); }
Сперва код формирует полностью квалифицированное имя файла и открывает указанный файл для записи. Затем в файл анимации выводится информация заголовка. Заголовок содержит количество объектов и количество кадров. Информация заголовка необходима для того, чтобы во время загрузки вы знали сколько данных нужно прочесть. Подробная структура файла показана на рис. 11.22.
Рис. 11.22. Структура формата файла анимации
На рис. 11.22 видно, что файл анимации состоит из заголовка, за которым следуют имена объектов, а далее располагается информация о ключевых кадрах.
Как только заголовок записан, я в цикле перебираю имена всех трехмерных объектов и записываю их в файл. Я делаю это для того, чтобы при загрузке у меня была возможность динамически загружать трехмерные объекты, участвующие в анимации.
Далее следует информация о кадрах. Код в цикле перебирает все кадры анимации и выводит информацию о задержке, вращении, масштабировании и местоположении каждого присутствующего в кадре объекта. Цикл продолжается до тех пор, пока не будет выведена информация обо всех кадрах.
В самом конце функции я закрываю файл и устанавливаю имя анимации. Поскольку имя анимации это имя файла, его не надо сохранять внутри файла.
Функция загрузки читает ранее сохраненные функцией записи данные. Звучит просто, не так ли? А вот и код функции:
void C3DAnimation::vLoad(char *szFileName) { FILE *fp; int i, j; int iNumObjs; int iNumFrames; char szFullFileName[512]; // Создание квалифицированного имени файла sprintf(szFullFileName, "Data\\Anims\\%s.anim", szFileName); // Открытие файла для чтения fp = fopen(szFullFileName, "rb"); if(fp == NULL) { return; } // Сброс объектов в состояние по умолчанию vReset(); // Чтение заголовка // Количество объектов fread(&iNumObjs, 1, sizeof(int), fp); // Количество кадров fread(&iNumFrames, 1, sizeof(int), fp); // Загрузка информации об объектах for(i = 0; i < iNumObjs; i++) { // Чтение имени объекта fread(&m_szObjectName[i][0], 32, sizeof(char), fp); // Загрузка данных объекта iNewObj(&m_szObjectName[i][0]); } // Выделение памяти для кадров for(i = 0; i < iNumFrames; i++) { vNewFrame(); } // Чтение информации о ключевом кадре for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { // Задержка fread(&m_keyFrames[i][j]->m_lTimeDelay, 1, sizeof(long), fp); // Вращение fread(&m_keyFrames[i][j]->m_vecRot, 1, sizeof(D3DXVECTOR3), fp); // Масштаб fread(&m_keyFrames[i][j]->m_vecScale, 1, sizeof(D3DXVECTOR3), fp); // Местоположение fread(&m_keyFrames[i][j]->m_vecTrans, 1, sizeof(D3DXVECTOR3), fp); } } // Закрываем файл анимации fclose(fp); // Сохраняем имя анимации strcpy(m_szAnimName, szFileName); }
Ход выполнения функции загрузки показан на рис. 11.23.
Рис. 11.23. Ход выполнения функции загрузки
На рисунке, как и в коде видно, как функция читает данные анимации. В первых строках кода создается полностью квалифицированное имя файла, и затем открывается указанный файл. Потом код выполняет инициализацию анимации, если она содержит какие-то данные, и загружает данные заголовка.
После загрузки данных заголовка функция в цикле считывает указанное количество имен трехмерных объектов. Прочитав очередное имя, код загружает модель трехмерного объекта. Это продолжается, пока не будут загружены все объекты.
После того, как загружены данные трехмерных моделей, функция выделяет память для каждого содержащегося в файле кадра. Когда требуемая память выделена, считываются сами данные кадров.
И в самом конце код закрывает файл и сохраняет имя анимации в данных класса анимации. Вот и все!
Функция задания трехмерного устройства применяется для установки внутреннего указателя на устройство Direct3D. Этот указатель необходим для загрузки трехмерных моделей из X-файлов. Вот код функции:
void C3DAnimation::vSet3DDevice(LPDIRECT3DDEVICE9 pd3dDevice) { m_pd3dDevice = pd3dDevice; }
К данному моменту я показал вам архитектуру проекта и внутреннее устройство класса анимации, используемого в редакторе анимации. В этом разделе я расскажу о логике работы самой программы редактора. Чтобы следить за изложением, загрузите файл main.cpp. Кроме того, если хотите видеть о чем я говорю, запустите саму программу редактора.
Ход выполнения программы редактора очень похож на работу других программ, описываемых в этой книге. Чтобы увидеть ход выполнения программы, взгляните на рис. 11.24.
Рис. 11.24. Ход выполнения программы D3D_AnimationEditor
На рис. 11.24 видно как главная функция инициализирует DirectInput, клавиатуру, Direct3D, интерфейс, панель команд, данные анимации и освещение. После завершения инициализации цикл обработки сообщений контроллирует поступающие от пользователя данные и визуализирует трехмерную сцену, пока не будет завершена работа программы. Здесь нет ничего принципиально нового за исключением функции инициализации анимации.
Функция инициализации анимации всего лишь устанавливает внутренний указатель на устройство Direct3D, присваивая ему значение, полученное при вызове функции InitD3D(). Вот как выглядит код этой процедуры:
void vInitAnimation(void) { // Установка трехмерного устройства animTest.vSet3DDevice(g_pd3dDevice); }
В функции я обращаюсь к глобальному объекту класса анимации. Функция vSet3DDevice() вызывается чтобы установить его внутренний указатель на устройство визуализации. Я делаю это потому, что объекту класса анимации необходим указатель на устройство Direct3D для загрузки трехмерных объектов, образующих сцену. Вам надо вызвать эту функцию только один раз, так что процедура инициализации — самое подходящее для этого место.
Чтобы начать редактирование анимации, вам необходима анимируемая сцена. Как я говорил ранее, сцена состоит из трехмерных объектов, поэтому вы должны загрузить какие-нибудь объекты для редактирования. Здесь в игру вступает функция vLoadObject(). Она загружает составляющие сцену объекты. Когда вы щелкаете по расположенной на панели команд кнопке Load Objects выполняется следующий код:
void vLoadObject(void) { // Сброс текущей анимации animTest.vReset(); // Загрузка заданных объектов animTest.iNewObj("droid_still"); animTest.iNewObj("radar_dish"); // Визуализация сцены vRender(); // Обновление панели команд vUpdateToolbarStats(); }
Первая вещь, которую делает функция загрузки объектов, — инициализация объекта класса анимации. Это действие удаляет все проделанные с анимацией к данному моменту манипуляции и возвращает ее к исходному состоянию.
Следующий фрагмент кода обращается к функции создающему новый объект методу класса анимации и загружает объекты из двух X-файлов с именами droid_still и radar_dish. Объект droid_still — это механоид, а объект radar_dish — это небольшая антена радара.
Следующий блок кода вызывает функцию визуализации для обновления сцены с только что обновленными объектами класса анимации. Поскольку в начале функции я очистил данные анимации, визуализируемая сцена будет пуста, если не считать изображения пола, на котором будут стоять объекты. Помните, что функция загрузки объектов всего лишь добавляет объекты к сцене; она не создает ключевых кадров. Именно поэтому вы не увидите объекты сразу же. Сперва надо добавить ключи.
В конце я вызываю функцию vUpdateToolbarStats(). Она выводит в панели инструментов значения вращения и местоположения объекта в текущем кадре.
Теперь, когда на вашей сцене есть трехмерные объекты, вам требуются данные кадров. Щелкните по кнопке New Frame и в обработчике сообщений будет выполнен следующий код:
case ID_BUTTON_NEWFRAME: // Создание нового кадра анимации animTest.vNewFrame(); SetActiveWindow(g_hWnd); // Обновление информации на панели инструментов vUpdateToolbarStats(); break;
Как видно в коде, при щелчке по кнопке New Frame вызываются несколько функций объекта класса анимации. Вызов метода класса vNewFrame() создает новый кадр для каждого объекта сцены. Поскольку у вас загружено два объекта, будет создано два кадра.
Следующая строка кода делает окно редактирования активным. Я делаю это для того, чтобы после создания нового кадра вам не надо было щелкать по окну редактора для его активизации.
И в самом конце я вызываю функцию обновления состояния панели инструментов. Замечательно то, что теперь вы должны увидеть объекты сцены на экране. У вас появились данные кадров и программе есть что показать!
У вас есть кадр и объекты, что теперь? Давайте займемся редактированием! Щелкните по кнопке Next Obj, пока антена не станет отображаться нормально, а не в виде каркаса. При каждом щелчке по кнопке Next Obj будет выполняться следующий код в обработчике сообщений:
case ID_BUTTON_NEXTOBJ: // Увеличение номера текущего объекта g_iCurObj++; // Если достигли конца, вернемся к началу if(g_iCurObj >= animTest.m_iNumObjects) { g_iCurObj = 0; } SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;
Код перехода к следующему объекту начинается с увеличения глобальной переменной, хранящей номер текущего объекта. Затем я проверяю, не превысило ли значение счетчика количество объектов в сцене. Если да, я возвращаюсь к началу списка объектов. Код перехода к предыдущему объекту работает почти так же, как рассмотренная функция за исключением того, что значение глобальной переменной с номером кадра уменьшается, а не увеличивается.
Теперь, когда антена радара выбрана, необходимо переместить ее. Возможно, вы уже заметили, что антена находится на земле, а не наверху механоида! Это легко исправить, ведь все что надо сделать — ввести соответствующие значения в текстовых полях, задающих местоположение объекта. В данном случае нам нужно среднее поле в левом столбце. Укажите в нем значение 20.0, и увидите как антена займет свое место на верхушке робота. Вуаля! Разве не здорово? Если все сделано правильно, вы увидите антену радара на предназначенном для нее месте. Код, обрабатывающий это перемещение, выглядит так:
case ID_EDITBOX_Y: memset(szBuffer, 0x00, 32); GetWindowText(hEDITBOX_Y, szBuffer, 32); if(animTest.m_iNumFrames > 0) { animTest.m_keyFrames [g_iCurObj][animTest.m_iCurFrame]->m_vecTrans.y = (float)atof(szBuffer); } break;
В коде я извлекаю значение из поля редактирования и преобразую его в число с плавающей запятой. Затем я устанавливаю новое значение смещения по оси Y выбранного объекта в текущем кадре. Фактически, все что я сделал — взял новое значение местоположения из поля редактирования и применяю его к антене радара. Здорово?
Вы можете поэкспериментировать и с перемещением робота. Для этого перебирайте объекты, пока не будет выбран робот. После того, как робот выбран, изменяйте значения его местоположения, пока робот не займет то местоположение, которое вам нравится.
Рядом с разобранным только что кодом вы увидите другие похожие фрагменты кода. Это объясняется тем, что аналогичный код применяется для каждой оси перемещения и для каждой оси вращения. Все эти фрагменты работают одинаково. Единственное различие заключается в изменяемом векторе.
Анимация пока, возможно, не слишком захватывающая. Теперь, когда антена радара находится на предназначенном для нее месте, добавьте к анимации несколько ключевых кадров. Если вы щелкните по кнопке New Frame три раза, в вашей анимации будет четыре кадра. Эти изменения отразятся в отладочной информации, выводимой в окне редактирования.
Помните, что ключевой кадр создается для каждого объекта сцены; так что, если вы щелкнули по кнопке New Frame три раза, всего у вас будет восемь ключевых кадров.
Теперь у вас есть пачка кадров и некуда идти. Выберите антену радара, если вы еще этого не сделали, и затем перебирайте кадры, пока текущим не станет кадр с номером 2. При щелчке по кнопке Next Frame выполняется следующий код:
case ID_BUTTON_NEXTFRAME: animTest.iNextFrame(); SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;
Разве вы не любите простой код? В этом коде я просто обращаюсь к функции объекта класса анимации, выполняющей переход к следующему кадру и забываю об этом. Функция выполняет все необходимое для смены текущего кадра, и я не должен волноваться об этом в коде редактора.
Теперь, когда у вас выбран второй кадр, измените поворот антены радара, чтобы ее центр был направлен немного влево. Сделав это, перейдите к третьему кадру и поверните антену еще на несколько градусов. Продолжайте процесс, пока не вернетесь к кадру номер 1. Затем щелкните по кнопке Start/Stop Anim, чтобы воспроизвести небольшую анимацию, которую вы только что создали. Будет выполнен следующий код:
case ID_BUTTON_STARTSTOP: // Если анимация активна, остановить ее if(g_iAnimActive) { g_iAnimActive = 0; } // Если анимация остановлена, запустить ее else { g_iAnimActive = 1; } SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;
В коде воспроизведения я переключаю значение флага g_iAnimActive в зависимости от его текущего состояния. Это указывает системе визуализации, что она должна делать с классом анимации во время визуализации. Когда анимация активна, функция визуализации продвигается по ней после каждого цикла отображения. Если анимация остановлена, функция визуализации просто отображает объекты и не меняет текущий кадр анимации.
Пока вы редактировали анимацию и воспроизводили ее, но я не показал вам как визуализировать объекты, составляющие анимируемую сцену. В следующем фрагменте кода приведена большая часть функции визуализации, необходимой для отображения сцены:
// Очистка вторичного буфера синим цветом g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_RGBA(200, 250, 255, 255), 1.0f, 0); // Начало сцены g_pd3dDevice->BeginScene(); // Установка материала по умолчанию g_pd3dDevice->SetMaterial(&g_mtrlDefault); // Установка режима сплошной заливки g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); // Визуализация пола vDraw3DObject(D3DXVECTOR3( 0.0, 0.0, 0.0) , D3DXVECTOR3(200.0, 200.0, 200.0), D3DXVECTOR3( 90.0, 0.0, 0.0), 0); // Визуализация трехмерных объектов if(animTest.m_iNumFrames && animTest.m_iNumObjects) { for(int i = 0; i < animTest.m_iNumObjects; i++) { // Объекты не являющиеся текущими отображаем в каркасном режиме if(i != g_iCurObj) { g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); } // Текущий объект отображаем в режиме сплошной заливки else { g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); } // Установка текущего кадра iCFrame = animTest.m_iCurFrame; // Визуализируем объекты, используя информацию кадра // хранящуюся в объекте анимации animTest.m_objObject[i]->vDisplayXYZ( animTest.m_keyFrames[i][iCFrame]->m_vecTrans.x, animTest.m_keyFrames[i][iCFrame]->m_vecTrans.y, animTest.m_keyFrames[i][iCFrame]->m_vecTrans.z, animTest.m_keyFrames[i][iCFrame]->m_vecRot.x, animTest.m_keyFrames[i][iCFrame]->m_vecRot.y, animTest.m_keyFrames[i][iCFrame]->m_vecRot.z, animTest.m_keyFrames[i][iCFrame]->m_vecScale.x, animTest.m_keyFrames[i][iCFrame]->m_vecScale.y, animTest.m_keyFrames[i][iCFrame]->m_vecScale.z); // Анимация объекта if(g_iAnimActive) { animTest.m_lCurTime++; // Переход к следующему кадру if(animTest.m_lCurTime >= animTest.m_keyFrames[i] [animTest.m_iCurFrame]->m_lTimeDelay) { animTest.m_iCurFrame++; bFrameChanged = 1; animTest.m_lCurTime = 0; // Сброс счетчика кадров if(animTest.m_iCurFrame >= animTest.m_iNumFrames) { animTest.m_iCurFrame = 0; } } } } }
Я знаю, что код визуализации объектов может выглядеть сложным, но все не так плохо. Сперва я вызываю мою функцию vDraw3DObject() чтобы нарисовать пол. Если вы не заметили, пол я рисую для того, чтобы облегчить расположение объектов. Он представляет собой большую серую решетку, расположенную вдоль оси Y.
Затем начинается код настоящей анимации. Сперва я выполняю проверку, чтобы убедиться, что существуют объекты и кадры. Если в анимации нет объектов или кадров, я пропускаю весь остальной код, не пытаясь что-либо отображать на экране. Вы обязаны делать эту проверку, чтобы не привести программу к краху.
Поскольку анимационная сцена может содержать несколько объектов, я создал цикл, который перебирает каждый объект сцены. Логика цикла изображена на рис. 11.25.
Рис. 11.25. Логика визуализации
Если в цикле обрабатывается тот объект, который выбран в данный момент в редакторе, он отображается закрашенным, если нет — объект отображается в виде каркаса. Сама визуализация выполняется путем вызова функции vDisplayXYZ(). Она получает информацию о местоположении, вращении и масштабировании объекта и визуализирует его согласно этим данным. Данные о местоположении вращении и масштабировании берутся из соответствующих векторов объекта, хранящихся в данных кадра.
Вторая часть цикла анимирует объект, если установлен глобальный флаг анимации. Если он установлен, код увеличивает счетчик времени в объекте класса анимации. Затем код проверяет, не вышло ли значение счетчика за предел, заданный в данных текущего кадра. Если да, выполняется смена кадра и счетчик времени обнуляется. Если номер текущего кадра превышает общее количество кадров, счетчик кадров возвращается к начальному, нулевому значению.
Оставшаяся часть функции визуализации выводит отладочную информацию, которую вы видите в окне редактирования. В ней нет ничего нового, и вы можете исследовать ее самостоятельно.
В функции vCheckInput() я проверяю состояние клавиш, отвечающих за перемещение камеры или выбранного в данный момент объекта по сцене. Управление камерой осуществляется клавишами перемещения курсора, а перемещение объектов — символьными клавишами, расположенными в левой части клавиатуры. Чтобы определить какая клавиша какое действие выполняет, взгляните на код функции.
В коде программы есть несколько комментариев, содержащих предложения по добавлению функциональности. Взгляните на эти предложения и попытайтесь самостоятельно реализовать некоторые из них. Я не написал код за вас чтобы вы могли изучить работу редактора и вступить на путь создания собственного редактора для вашей стратегической игры. Наряду с комментариями я предлагаю вам попробовать реализовать следующие усовершенствования:
netlib.narod.ru | < Назад | Оглавление | Далее > |