Формат 3DS — не лучший способ хранить анимацию, тем не менее совсем недавно это был самый популярный способ анимации в играх. Этот урок не раскроет всех ньюансов, но даст хороший толчок для написания ещё лучшей системы анимации. Этот урок должен стать шагом на пути к скелетной анимации, которая является ЛУЧШИМ способом создания красивых и реалистичных персонажей. Анимация в этом уроке представляет собой простое вращение и перемещение обьектов. Сразу запомните одну вещь: НУЛЕВОЙ кадр анимации игнорируется.

Итак, как это всё работает. Если вы не понимаете основ загрузки 3DS-моделей, вернитесь к предыдущему уроку. Код для этого урока написан на его основе.

Вот основы покадровой анимации: вы сохраняете значения позиции, вращения и изменения размеров в ключевых кадрах, и они интерполируются между данными. Скажем, вы начали с кадра 0, в котором куб находится в положении (0,0,0), затем переходите в кадр 60 и передвигаете куб в координаты (10, 10, 10). Теперь при воспроизведении анимации вы увидите, как куб плавно перемещается из старых координат в новые. Точно так же работает вращение и изменение размеров. Вы просто устанавливаете «КЛЮЧЕВЫЕ» кадры, остальное рассчитывается за вас. Ложка дегтя в анимации на основе .3ds в том, что тогда как перемещение и изменение размеров сохраняется для каждого кадра, вращение сохраняется только для каждого КЛЮЧЕВОГО кадра. Это значит, что если данные перемещения и scale мы уже имеем интерполированными, данные вращения нам придется интерполировать вручную.

Итак, всё, что нужно делать нового — читать значения Position/Scale для каждого кадра, и если для следующего кадра оно не меняется — просто оставляем старые. Для вращения нам нужно будет получить значения вращения и оси вращения для ключевых кадров, а затем интерполировать эти значения между ключевыми кадрами, а если вращения нет — просто установить его в 0.

Изменяем файл main.h, приводим его к следующему виду:

#ifndef _MAIN_H
#define _MAIN_H// Хидеры, необходимые для работы программы
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <glgl.h>
#include <glglu.h>
#include <vector>
#include <math.h>using namespace std;

// Обьявим глобальные переменные, ширину, высоту и глубину цвета экрана
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
#define SCREEN_DEPTH 16

// Глобальные параметры окна; будут доступны из других файлов:
extern HWND  g_hWnd;
extern RECT  g_rRect;
extern HDC   g_hDC;
extern HGLRC g_hRC;
extern HINSTANCE g_hInstance;

struct CVector3{
float x,y,z;
};

struct CVector2{
float x,y;
};

// Это структура полигона. Она используется для индексирования массивов координат
// вершин и текстур. Эта информация сообщает нам о том, какие номера вершин в массиве
// какому полигону принадлежат. То же самое касается текстурных координат.
struct tFace
{
int vertIndex[3];           // indicies for the verts that make up this triangle
int coordIndex[3];          // indicies for the tex coords to texture this face
};

// Эта структура хранит информацию о материале. Это может быть текстурная карта света.
// Некоторые значения не используются, но я оставил их, чтобы могли увидеть их для
// примера.
struct tMaterialInfo
{
char  strName[255];         // Имя текстуры
char  strFile[255];         // Имя файла текстуры
BYTE  color[3];             // Цвет обьекта (R, G, B)
int   texureId;             // ID текстуры
float uTile;                // u-tiling текстуры (Сейчас не используется)
float vTile;                // v-tiling текстуры (Сейчас не используется)
float uOffset;              // u-offset текстуры (Сейчас не используется)
float vOffset;              // v-offset текстуры (Сейчас не используется)
};

// Содержит всю информацию о модели/сцене.
// В реальном проекте лучше оберните всё это в класс с
// функциями вроде LoadModel(…); DrawObject(…); DrawModel(…); DestroyModel(…);
struct t3DObject
{
int  numOfVerts;            // Число вершин в модели
int  numOfFaces;            // Число полигонов в модели
int  numTexVertex;          // Число текстурных координат
int  materialID;            // ID текстуры для использования, индекс массива текстур
bool bHasTexture;           // TRUE если есть текстурная карта для этого обьекта
char strName[255];          // Имя обьекта
CVector3  *pVerts;          // Массив вершин обьекта
CVector3  *pNormals;            // Нормали обьекта
CVector2  *pTexVerts;           // Текстурные координаты
tFace *pFaces;              // Полигоны обьекта

int  positionFrames;        // Число кейфреймов перемещения
int  rotationFrames;        // Число кейфреймов вращения
int  scaleFrames;       // Число кейфреймов масштабирования

CVector3 vPivot;        // Точка опоры обьекта

vector<CVector3> vPosition; // Массив позиций обьекта
vector<CVector3> vRotation; // Массив вращений обьекта
vector<CVector3> vScale;    // Массив размеров обьекта

vector<float> vRotDegree;   // Углы вращения обьекта
};

// Содержит информацию о модели. Тоже неплохо бы обернуть в класс. Мы будем использовать
// класс вектора из STL (Standart Template Library) чтобы уменьшить трудности при связывании
// параметров.
struct t3DModel
{
int numOfObjects;           // Число обьектов в модели
int numOfMaterials;         // Число материалов модели
vector<tMaterialInfo> pMaterials;   // Число обьектов материалов (текстуры и цвета)
vector<t3DObject> pObject;      // Список обьектов в модели

int numberOfFrames; // Число кадров анимации для данной модели (как минимум 1)
int currentFrame;   // Текущий кадр анимации модели
};

// Прототип главной функции программы — WinMain
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hprev, PSTR cmdline, int ishow);

// Прототип функции обработки сообщений
LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

// Функция — главный цикл программы
WPARAM MainLoop();

// Функция, создающая окно
HWND CreateMyWindow(LPSTR strWindowName, int width, int height, DWORD dwStyle, bool bFullScreen, HINSTANCE hInstance);

// Функция, устанавливающая формат пиксела
bool bSetupPixelFormat(HDC hdc);

// Прототип функции, устанавливающей размеры окна OpenGL
void SizeOpenGLScreen(int width, int height);

// Функция, инициализирующая OpenGL
void InitializeOpenGL(int width, int height);

// Общая инициализация
void Init(HWND hWnd);

// Функция, которая собственно рисует сцену
void RenderScene();

// Де-инициализация
void DeInit();

#endif

 

В класс CLoad3DS добавим некоторые константы и функции.

Файл 3ds.h:

// Константы для чтения анимации:
#define KEYFRAME            0xB000  // Даёт нам знать, что мы считываем ключевой кадр
#define KEYFRAME_MESH_INFO      0xB002
#define KEYFRAME_OBJECT_NAME        0xB010
#define KEYFRAME_START_AND_END      0xB008
#define PIVOT               0xB013
#define POSITION_TRACK_TAG      0xB020
#define ROTATION_TRACK_TAG      0xB021
#define SCALE_TRACK_TAG         0xB022// Новые функции:
// Читает chunk-и ключевых кадров (данные анимации
void ProcessNextKeyFrameChunk(t3DModel *pModel, tChunk *);// Читает позицию для каждого кадра анимации
void ReadKeyFramePositions(t3DModel *pModel, tChunk *pPreviousChunk);

// Читает вращения для ключевых кадров
void ReadKeyFrameRotations(t3DModel *pModel, tChunk *pPreviousChunk);

// Читает данные scale для ключевых кадров
void ReadKeyFrameScales(t3DModel *pModel, tChunk *pPreviousChunk);

// Устанавливает текущую модель, для которой читается анимация
void SetCurrentObject(t3DModel *pModel, char *strObjectName);

// И новые переменные
// Эти две используются в процессе загрузки для хранения информации chunk-ов
tChunk *m_CurrentChunk;
tChunk *m_TempChunk;

// Хранит обьект, для которого читаются данные анимации
t3DObject *m_CurrentObject;

 

Изменяем файл 3ds.cpp. Я приведу его полностью.

#include «main.h»
#include «3ds.h»int gBuffer[50000] = {0};   // Используется для чтения нежелательных данных// В этом файле находится весь код, необходимый для загрузки файлов .3ds.
// Как оно работает: вы загружаете chunk, затем проверяете его ID.
// В зависимости от его ID, загружаете информацию, хранящуюся в chunk-е.
// Если вы не хотите читать эту информацию, читаете дальше неё.
// Вы знаете, как много байт нужно пропустить, так как каждый chunk хранит
// свою длинну в байтах.

///////////////////////////////// ROUND FLOAT «»»»»»»»»»\*
/////
/////   Новая функция: округляет float до нуля, если он меньше 0.001 или больше -0.001
/////
///////////////////////////////// ROUND FLOAT «»»»»»»»»»\*

float RoundFloat(float number)
{

// По какой-то странной причине когда читаются очень маленькие числа,
// они сохраняются с какой-то погрешностью, которая некорректно обрабатывается
// OpenGL. Так что мы просто округлим их до 0. Разницы вы не заметите, а
// работать будет лучше.

// Если передано очень маленькое число, устанавливаем его в 0
if(number > 0 && number <  0.001f) number = 0;
if(number < 0 && number > 0.001f) number = 0;

// Возвращаем изменённый или неизменённый float
return number;
}

///////////////////////////////// CLOAD3DS «»»»»»»»»»\*
/////
/////   Конструктор инициализирует данные tChunk
/////
///////////////////////////////// CLOAD3DS «»»»»»»»»»\*

CLoad3DS::CLoad3DS()
{
m_CurrentChunk = new tChunk;
m_TempChunk = new tChunk;
}

///////////////////////////////// IMPORT 3DS «»»»»»»»»»\*
/////
/////   Вызывается клиентом для открытия, чтения и затем очистки .3ds
/////
///////////////////////////////// IMPORT 3DS «»»»»»»»»»\*

bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName)
{
char strMessage[255] = {0};

// Открываем .3ds файл
m_FilePointer = fopen(strFileName, «rb»);

// Убедимся, что указатель на файл верен (мы открыли файл)
if(!m_FilePointer)
{
sprintf(strMessage, «Unable to find the file: %s!», strFileName);
MessageBox(NULL, strMessage, «Error», MB_OK);
return false;
}

// Открыв файл, нужно прочитать хидер файла, чтобы убедится, что это 3DS.
// Если это верный файл, то первым ID chunk-а будет PRIMARY

// Читаем первый chunk файла, чтобы убедится, что это 3DS
ReadChunk(m_CurrentChunk);

// Убедимся, что это 3DS
if (m_CurrentChunk->ID != PRIMARY)
{
sprintf(strMessage, «Unable to load PRIMARY chuck from file: %s!», strFileName);
MessageBox(NULL, strMessage, «Error», MB_OK);
return false;
}

// Теперь начинаем чтение данных. ProcessNextChunk() — рекурсивная функция

// Начинаем загрузку обьектов вызовом рекурсивной функции
ProcessNextChunk(pModel, m_CurrentChunk);

// После прочтения всего файла нам нужно рассчитать нормали вершин
ComputeNormals(pModel);

// В конце подчищаем всё
CleanUp();

return 0;
}

///////////////////////////////// CLEAN UP «»»»»»»»»»\*
/////
/////   Функция чистит всю занятую память и закрывает файл
/////
///////////////////////////////// CLEAN UP «»»»»»»»»»\*

void CLoad3DS::CleanUp()
{

fclose(m_FilePointer);
delete m_CurrentChunk;
delete m_TempChunk;
}

///////////////////////////////// PROCESS NEXT CHUNK»»»»»»»»»»\*
/////
/////   Функция читает главную секцию файла, затем рекурсивно идёт глубже
/////
///////////////////////////////// PROCESS NEXT CHUNK»»»»»»»»»»\*

void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
t3DObject newObject = {0};      // Используется для добавления обьекта в список
tMaterialInfo newTexture = {0};     // Используется для добавления материала
int version = 0;                    // Версия файла

m_CurrentChunk = new tChunk;        // Текущий chunk для загрузки

// Продолжаем читать подсекции, пока не дойдем до общей длинны файла.
// После чтения ЧЕГО УГОДНО, увеличиваем прочитанные байты и сравниваем
// их с общей длинной.
while (pPreviousChunk->bytesRead < pPreviousChunk->length)
{
// Читаем следующий chunk
ReadChunk(m_CurrentChunk);

// Получаем chunk ID
switch (m_CurrentChunk->ID)
{
case VERSION:           // Версия файла

// Читаем версию файла и добавляем прочитанные байты в переменную bytesRead
m_CurrentChunk->bytesRead += fread(&version, 1, m_CurrentChunk->length
m_CurrentChunk->bytesRead, m_FilePointer);

// Если версия файла больше 3, выведем предупреждение, что могут
// возникнуть проблемы.
if (version > 0x03)
MessageBox(NULL, «This 3DS file is over version 3 so it may load incorrectly», «Warning», MB_OK);
break;

case KEYFRAME:

// Здесь мы начинаем читать информацию о ключевых кадрах.
// Она читается в КОНЦЕ файла.

// Рекурсивно считываем chunk
ProcessNextKeyFrameChunk(pModel, m_CurrentChunk);

// Увеличиваем счетчик считанных байт
m_CurrentChunk->bytesRead += fread(gBuffer, 1, m_CurrentChunk->length
m_CurrentChunk->bytesRead, m_FilePointer);
//          MessageBox(NULL,»test»,»e»,MB_OK);
break;

case OBJECTINFO:        // Содержит версию меша
// Этот chunk содержит версию меша. Также это заголовок для chunk-ов MATERIAL
// и OBJECT. Отсюда мы начинаем читать информацию материалов и обьектов.

// Читаем следующий chunk
ReadChunk(m_TempChunk);

// Получаем версию меша
m_TempChunk->bytesRead += fread(&version, 1, m_TempChunk->length
m_TempChunk->bytesRead, m_FilePointer);

// Увеличиваем bytesRead на число прочитанных байт
m_CurrentChunk->bytesRead += m_TempChunk->bytesRead;

// Переходим к следующему chunk-у, это будет MATERIAL, затем OBJECT
ProcessNextChunk(pModel, m_CurrentChunk);
break;
case MATERIAL:          // Содержит информацию о материале

// Этот chunk — хидер для информации о материале

// Увеличиваем число материалов
pModel->numOfMaterials++;

// Добавляем пустую структуру текстуры в наш массив текстур.
pModel->pMaterials.push_back(newTexture);

// Вызываем функцию, обрабатывающую материал
ProcessNextMaterialChunk(pModel, m_CurrentChunk);
break;

case OBJECT:            // Хранит имя читаемого обьекта

// Этот chunk — хидер для chunk-ов, хранящих информацию обьекта.
// Также он хранит имя обьекта.

// Увеличиваем счетчик обьектов
pModel->numOfObjects++;

// Добавляем новый элемент tObject к списку обьектов
pModel->pObject.push_back(newObject);

// Инициализируем обьект и все его данные
memset(&(pModel->pObject[pModel->numOfObjects 1]), 0, sizeof(t3DObject));

// Получаем и сохраняем имя обьекта, затем увеличиваем счетчик прочитанных байт
m_CurrentChunk->bytesRead += GetString(pModel->pObject[pModel->numOfObjects 1].strName);

// Переходим к чтению оставшейся информации обьекта
ProcessNextObjectChunk(pModel, &(pModel->pObject[pModel->numOfObjects 1]), m_CurrentChunk);
break;

default:

// Остальные chunk-и, которые нам не нужны, будут обработаны здесь. Нам
// всё ещё нужно прочитать в «мусорную» переменную неизвестные или игнорируемые
// chunk-и и увеличить счетчик прочитанных байт.
m_CurrentChunk->bytesRead += fread(gBuffer, 1, m_CurrentChunk->length
m_CurrentChunk->bytesRead, m_FilePointer);
break;
}

// Прибавим прочитанные байты последнего chunk-а к счетчику
pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
}
delete m_CurrentChunk;
m_CurrentChunk = pPreviousChunk;
}

///////////////////////////////// PROCESS NEXT KEYFRAME CHUNK «»»»»»»»»»\*
/////
/////   Эта функция и сохраняет данные анимации.
/////   Мы получаем имя обьекта, для которого читается анимация, число анимированных кадров,
/////   точку опоры, размерность, вращение и перемещение для ключевых кадров. Затем мы
/////   интерполируем эти данные между ключевыми кадрами.
/////
///////////////////////////////// PROCESS NEXT KEYFRAME CHUNK «»»»»»»»»»\*

void CLoad3DS::ProcessNextKeyFrameChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
//  MessageBox(NULL,»test»,»e»,MB_OK);
char strKeyFrameObject[50] = {0};   // Хранит имя текущего обьекта
float temp = 0.0f;          // Для смены координат y и z точки опоры

// Инициализируем новый chunk для работы с ним
m_CurrentChunk = new tChunk;

// Продолжаем читать секции, пока не достигаем конца подсекции
while (pPreviousChunk->bytesRead < pPreviousChunk->length)
{

//  MessageBox(NULL,»yes»,»e»,MB_OK);

// Читаем следующий chunk
ReadChunk(m_CurrentChunk);

// Проверяем, что за секцию мы читаем
switch (m_CurrentChunk->ID)
{
case KEYFRAME_MESH_INFO:    // Говорит нам, что описывается информация для нового обьекта

// Это сообщает нам, что читается анимация для следующего обьекта, так что
// нужно рекурсивно заново читать chunk
ProcessNextKeyFrameChunk(pModel, m_CurrentChunk);
break;

case KEYFRAME_OBJECT_NAME:  // Хранит имя текущего обьекта

// Получаем имя обьекта, для которого читаются данные анимации
m_CurrentChunk->bytesRead += GetString(strKeyFrameObject);

// Теперь, узнав, для какого обьекта читаются данные, устанавливаем m_CurrentObject.
// Таким образом у мы получаем указатель на обьект модели, содержащий данные
// анимации.
SetCurrentObject(pModel, strKeyFrameObject);

// Читаем 2 переданных флага и иерархический номер (3 short-а — не используется
// в этом уроке).
m_CurrentChunk->bytesRead += fread(gBuffer, 1,
m_CurrentChunk->length m_CurrentChunk->bytesRead, m_FilePointer);
break;

case KEYFRAME_START_AND_END:        // Этот chunk хранит начальный и конечный кадры.

// Читаем начальный и конечный кадры. Просто переписываем начальный кадр, т.к.
// подразумевается, что мы всегда начинаем с 0 кадра
m_CurrentChunk->bytesRead += fread(&(pModel->numberOfFrames), 1, 4, m_FilePointer);
m_CurrentChunk->bytesRead += fread(&(pModel->numberOfFrames), 1, 4, m_FilePointer);
break;

case PIVOT:         // Хранит точку опоры обьекта

// Здесь мы читаем 3 float-а (X, Y, Z) для точки опоры обьекта.
// Точка опоры — локальная ось, вокруг которой вращается обьект. По умолчанию
// её координаты (0, 0, 0), но они могут быть изменены.
m_CurrentChunk->bytesRead +=
fread(&(m_CurrentObject->vPivot), 1, sizeof(CVector3), m_FilePointer);

// Так как в 3DS Max оси Z и Y перевернуты, меняем местами координаты
// z и y точки опоры, затем делаем отрицательным Z.
temp = m_CurrentObject->vPivot.y;
m_CurrentObject->vPivot.y = m_CurrentObject->vPivot.z;
m_CurrentObject->vPivot.z = temp;
break;

case POSITION_TRACK_TAG:    // Хранит перемещение для каждого кадра

// Теперь нам нужно прочитать позицию для каждого кадра анимации
ReadKeyFramePositions(pModel, m_CurrentChunk);
break;

case ROTATION_TRACK_TAG:    // Хранит вращение для каждого КЛЮЧЕВОГО кадра

// Теперь нам нужно прочитать вращение для каждого КЛЮЧЕВОГО кадра анимации.
// Данные вращения для каждого простого кадра не хранятся как, например,
// перемещения, так что нам нужно интерполировать их вручную.
ReadKeyFrameRotations(pModel, m_CurrentChunk);
break;

case SCALE_TRACK_TAG:       // Хранит scale-значения для каждого кадра

// Теперь читаем значения размерности для каждого кадра анимации
ReadKeyFrameScales(pModel, m_CurrentChunk);
break;

default:

// Читаем в «мусор» ненужные/неизвестные данные
m_CurrentChunk->bytesRead += fread(gBuffer, 1,
m_CurrentChunk->length m_CurrentChunk->bytesRead, m_FilePointer);
break;
}

// Увеличиваем счетчик прочитанных байт
pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
}

// Освобождаем текущий chunk и возвращаемся к предыдущему
delete m_CurrentChunk;
m_CurrentChunk = pPreviousChunk;
}

///////////////////////////////// READ KEYFRAME POSITIONS «»»»»»»»»»\*
/////
/////   Функция читает позицию текущего обьекта для каждого кадра
/////
///////////////////////////////// READ KEYFRAME POSITIONS «»»»»»»»»»\*

void CLoad3DS::ReadKeyFramePositions(t3DModel *pModel, tChunk *pPreviousChunk)
{
short frameNumber= 0, flags= 0, ignored= 0;
long  lunknown= 0;
int i;

// Эта функция будет читать положение обьекта для каждого кадра. Запомните, это
// положение изменяется для точки опоры, а от неё уже рассчитываем положение обьекта.
// Первые 5 значений в этом уроке игнорируются.

// Считываем в «мусор» игнорируемые данные
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);

// Читаем число кадров с изменением позиции, т.е. сколько раз обьект перемещался
pPreviousChunk->bytesRead += fread(&(m_CurrentObject->positionFrames), 1, sizeof(short), m_FilePointer);

// Читаем в «мусор» игнорируемые данные
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);

// Теперь нам нужно пройти через все кадры анимации и установить позицию
// обьекта для каждого кадра. Имеем мы одну или 50 позиций, изменённых за 100 кадров,
// нам нужно установить позиции в оставшиеся кадры.
for(i = 0; i <= (pModel->numberOfFrames + 1); i++)
{
// Здесь мы добавляем новый CVector3 в наш массив позиций. Он будет сохранять
// текущую позицию для текущего кадра анимации ‘i’. Используя STL vector, мы
// просто передаём конструктор CVector3.
m_CurrentObject->vPosition.push_back(CVector3());

// Если текущий кадр анимации входит в кадры изменения позиции, читаем следующую
// позицию для текущего кадра.
if(i < m_CurrentObject->positionFrames)
{
// Читаем текущий фреймбуфер
pPreviousChunk->bytesRead += fread(&frameNumber, 1, sizeof(short), m_FilePointer);

// Читаем в «мусор» неизвестный long
pPreviousChunk->bytesRead += fread(&lunknown, 1, sizeof(long), m_FilePointer);

// Читаем 3 float, содержащих (x,y,z) позиции. Запомните, CVector3 — это
// 3 флоата, так что передаём как размер sizeof(float)*3
pPreviousChunk->bytesRead +=
fread(&(m_CurrentObject->vPosition[i]), 1, sizeof(CVector3), m_FilePointer);

// Так как 3DS Max меняет местами Y<->Z, конвертируем наши координаты
float temp = m_CurrentObject->vPosition[i].y;
m_CurrentObject->vPosition[i].y = m_CurrentObject->vPosition[i].z;
m_CurrentObject->vPosition[i].z = temp;
}
// Иначе просто устанавливаем текущую позицию равной прошлой
else
{
m_CurrentObject->vPosition[i] = m_CurrentObject->vPosition[m_CurrentObject->positionFrames 1];
}
}

// Теперь нам нужно вычесть точку опоры из каждой вершины. 3DS файлы сохраняют
// вершины обьекта как ВЕРШИНА плюс точка опоры (плохо). Вы заметите, что мы
// также вычитаем позицию текущего кадра из каждой точка. Это потому, что
// 3DS файлы хранят позицию точки опоры для каждого кадра. Мы хотим, чтобы
// точка опоры начиналась с 0, иак чир будем вызывать glTranslate(), что будет
// перемещать первую позицию.
for(i = 0; i < m_CurrentObject->numOfVerts; i++)
{
// Вычитаем текущую позицию кадра и точку опоры из каждой вершины
m_CurrentObject->pVerts[i].x -= m_CurrentObject->vPosition[0].x + m_CurrentObject->vPivot.x;
m_CurrentObject->pVerts[i].y -= m_CurrentObject->vPosition[0].y + m_CurrentObject->vPivot.y;
m_CurrentObject->pVerts[i].z -= m_CurrentObject->vPosition[0].z + m_CurrentObject->vPivot.z;
}
}

///////////////////////////////// READ KEYFRAME ROTATIONS «»»»»»»»»»\*
/////
/////   Новая функция: читает вращение текущего обьекта на каждый кейфрейм
/////
///////////////////////////////// READ KEYFRAME ROTATIONS «»»»»»»»»»\*

void CLoad3DS::ReadKeyFrameRotations(t3DModel *pModel, tChunk *pPreviousChunk)
{
short frameNumber = 0, flags = 0, rotkeys = 0, ignored = 0;
long lunknown = 0;
float rotationDegree = 0;
vector<int> vFrameNumber;
vector<float> vRotDegree;
vector<CVector3> vRotation;
int i;

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

// Читаем в «мусор» игнорируемые данные
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1,  sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1,  sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignored, 1,  sizeof(short), m_FilePointer);

// Читаем число кадров вращения для анимации. Помните, эти данные сохраняются
// не для каждого кадра, а только для ключевых. Поэтому нам надо будет
// вручную интерполировать данные между каждым кейфреймом.
pPreviousChunk->bytesRead += fread(&(m_CurrentObject->rotationFrames), 1, sizeof(short), m_FilePointer);

// Читаем игнорируемые данные
pPreviousChunk->bytesRead += fread(&ignored, 1,  sizeof(short), m_FilePointer);

// Теперь нужно пройти через ВСЕ кадры анимации и установить вращение обьекта
// для каждого кадра. Нам нужно интерполировать вращение между ключевыми
// кадрами, если их более 1.
for(i = 0; i < m_CurrentObject->rotationFrames; i++)
{
// Добавляем новый CVector3 в массив вращений. Он будет хранить текущий угол
// вращения для кадра ‘i’. Используя STL vector, просто передадим конструктор
// CVector3 через копирующий конструктор.
vRotation.push_back(CVector3());

// Далее читаем номер кадра
pPreviousChunk->bytesRead += fread(&frameNumber, 1, sizeof(short), m_FilePointer);

// Добавляем номер кадра в список номеров кадров для дальнейшей интерполяции
vFrameNumber.push_back(frameNumber);

// Читаем неизвестные данные в «мусор»
pPreviousChunk->bytesRead += fread(&lunknown, 1, sizeof(long), m_FilePointer);

// Читаем текущий угол вращения для данного ключевого кадра.
pPreviousChunk->bytesRead += fread(&rotationDegree, 1, sizeof(float), m_FilePointer);

// Так как 3DS Max хранит угол в радианах, а не градусах, преобразуем значение.
// Конвертируем radian->degree (Radians * (180 / PI) = degrees)
rotationDegree = rotationDegree * (180.0f / 3.14159f);

// Добавляем угол вращения в список
vRotDegree.push_back(rotationDegree);

// Читаем ось, вокруг которой вращается обьект.
pPreviousChunk->bytesRead += fread(&(vRotation[i]), 1, sizeof(CVector3), m_FilePointer);

// Округляем углы во избежание ненужных проблем
vRotation[i].x = RoundFloat(vRotation[i].x);
vRotation[i].y = RoundFloat(vRotation[i].y);
vRotation[i].z = RoundFloat(vRotation[i].z);

// Преобразуем оси y<->z
float temp = vRotation[i].y;
vRotation[i].y = vRotation[i].z;
vRotation[i].z = temp;

// Делаем значение X отрицательным
vRotation[i].x *= 1;
}

// Теперь у нас есть все данные о вращении. Теперь, поскольку данные о вращении
// хранятся для ключевых кадров, нужно их интерполировать.

// Добавляем в список ось вращения
m_CurrentObject->vRotation.push_back(vRotation[0]);

// Добавляем угол вращения первого кадра в массив. Если вращения нет, угол == 0
m_CurrentObject->vRotDegree.push_back(vRotDegree[0]);

// Счетчик для текущего кадра вращения
int currentKey = 1;

// Нужно пройти через все кадры анимации ещё раз, чтобы интерполировать данные
// вращения. Если кадр вращения только один, просто устанавливаем остальные углы
// вращения в ноль.

// Проходим через все кадры анимации
for(i = 1; i <= (pModel->numberOfFrames + 1); i++)
{
// Проверяем, не равен ли текущий кадр числу кадров вращения
if(currentKey < m_CurrentObject->rotationFrames)
{
// Получаем текущий и предыдущий номера кадров и углы вращения.
int currentFrame = vFrameNumber[currentKey];
int previousFrame = vFrameNumber[currentKey 1];
float degree = vRotDegree[currentKey];

// Интерполируем углы вращения между текущим и предыдущим ключевыми
// кадрами. Алгоритм прост, просто проверяем как много кадров прошло от
// прошлого до текущего ключевого. Затем делим угол вращения на эту
// разницу. Например, скажем, есть кадр 0 и кадр 50. 50-0 = 50, то есть
// нужно задать вращение для пятидесяти кадров.
float rotDegree = degree / (currentFrame previousFrame);

// Добавляем угол и вектор вращения для этого кадра
m_CurrentObject->vRotation.push_back(vRotation[currentKey]);
m_CurrentObject->vRotDegree.push_back(rotDegree);

// Проверим, нужно ли переходить к след. кейфрейму
if(vFrameNumber[currentKey] <= i)
currentKey++;
}
// Иначе мы закончили с ключевыми кадрами
else
{
// Устанавливаем остальное вращение в 0, т.к. нам больше не нужно вращение
m_CurrentObject->vRotation.push_back(vRotation[currentKey 1]);
m_CurrentObject->vRotDegree.push_back(0.0f);
}
}
}

///////////////////////////////// READ KEYFRAME SCALE «»»»»»»»»»\*
/////
/////   Новая функция: читает значение scale для текущего обьекта и кадра
/////
///////////////////////////////// READ KEYFRAME SCALES «»»»»»»»»»\*

void CLoad3DS::ReadKeyFrameScales(t3DModel *pModel, tChunk *pPreviousChunk)
{
short frameNumber = 0, ignore = 0, flags = 0;
long lunknown = 0;
int i = 0;

// Как и данные перемещения, изменение размеров сохраняется для каждого
// кадра анимации. Это значит, нам не нужно интерполировать эти данные
// вручную. Первые 5 параметров игнорируются.
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);

// Читаем число кадров-scale. Если есть 100 кадров анимации и только 50
// кадров изменения размера, просто устанавливаем остальные 50 кадров в
// значение последнего кадра.
pPreviousChunk->bytesRead += fread(&(m_CurrentObject->scaleFrames), 1, sizeof(short), m_FilePointer);

// Читаем игнорируемые данные
pPreviousChunk->bytesRead += fread(&ignore, 1, sizeof(short), m_FilePointer);

// Теперь нам нужно пройти через ВСЕ кадры анимации и установить значение scale.
for(i = 0; i <= (pModel->numberOfFrames + 1); i++)
{
// Добавляем довый CVector3 в список.
m_CurrentObject->vScale.push_back(CVector3());

// Если текущий кадр меньше их общего количества, читаем его данные
if(i < m_CurrentObject->scaleFrames)
{
// Читаем текущий номер кадра
pPreviousChunk->bytesRead += fread(&frameNumber, 1, sizeof(short), m_FilePointer);

// Читаем неизвестный long
pPreviousChunk->bytesRead += fread(&lunknown, 1, sizeof(long), m_FilePointer);

// Читаем (X, Y, Z) scale-значения для текущего кадра. Мы будем передавать
// их в glScalef().
pPreviousChunk->bytesRead +=
fread(&(m_CurrentObject->vScale[i]), 1, sizeof(CVector3), m_FilePointer);

// Меняем местами Z и Y оси
float temp = m_CurrentObject->vScale[i].y;
m_CurrentObject->vScale[i].y = m_CurrentObject->vScale[i].z;
m_CurrentObject->vScale[i].z = temp;
}
// Иначе мы закончили с данными scale
else
{
// Устанавливаем текущее scale-значение в значение последнего кадра
m_CurrentObject->vScale[i] = m_CurrentObject->vScale[m_CurrentObject->scaleFrames 1];
}
}
}

///////////////////////////////// SET CURRENT OBJECT «»»»»»»»»»\*
/////
/////   Новая функция: устанавливает текущую модель, для которой будет читатся анимация
/////
///////////////////////////////// SET CURRENT OBJECT «»»»»»»»»»\*

void CLoad3DS::SetCurrentObject(t3DModel *pModel, char *strObjectName)
{
// Функция принимает модель и имя обьекта внутри этой модели.
// Затем в модели ищется обьект с соответствующим именем.

// Убедимся, что передано правильное имя обьекта
if(!strObjectName)
{
// Устанавливаем текущий обьект в NULL и выходим
m_CurrentObject = NULL;
return;
}

// Проходим через все обьекты в модели и сравниваем их имя с переданным
for(int i = 0; i < pModel->numOfObjects; i++)
{
// Проверяем совпадение имён
if(strcmp(pModel->pObject[i].strName, strObjectName) == 0)
{
// Получаем указатель на обьект с переданным именем
// Это будет обьект, для которого дальше будут считыватся данные анимации.
m_CurrentObject =&(pModel->pObject[i]);
return;
}
}

// Выводим сообщение об ошибке
MessageBox(NULL, «ERROR: No object in model with given name! (SetCurrentObject)», «Error!», MB_OK);

// Устанавливаем текущий обьект в NULL, т.к. не нашли его
m_CurrentObject = NULL;
}

///////////////////////////////// PROCESS NEXT OBJECT CHUNK «»»»»»»»»»\*
/////
/////   Функция сохраняет всю информацию об обьекте
/////
///////////////////////////////// PROCESS NEXT OBJECT CHUNK «»»»»»»»»»\*

void CLoad3DS::ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
// Текущий chunk, с которым работаем
m_CurrentChunk = new tChunk;

// Продолжаем читать эти chunk-и, пока не дошли до конца этой секции
while (pPreviousChunk->bytesRead < pPreviousChunk->length)
{
// Читаем следующую секцию
ReadChunk(m_CurrentChunk);

// Проверяем, что это за секция
switch (m_CurrentChunk->ID)
{
case OBJECT_MESH:       // Даёт нам знать, что мы читаем новый обьект

// Нашли новый обьект, прочитаем его информацию рекурсией
ProcessNextObjectChunk(pModel, pObject, m_CurrentChunk);
break;

case OBJECT_VERTICES:       // Вершины нашего обьекта
ReadVertices(pObject, m_CurrentChunk);
break;

case OBJECT_FACES:      // Полигоны обьекта
ReadVertexIndices(pObject, m_CurrentChunk);
break;

case OBJECT_MATERIAL:       // Имя материала обьекта

// Эта секция хранит имя материала, связанного с текущим обьектом. Это может быть
// цвет или текстурная карта. Эта секция также содержит полигоны, к которым
// привязана текстура (Если например на обьекте несколько текстур, или просто
// текстура наложено только на часть обьекта). Сейчас у нас будет только одна
// текстура на весь обьект, так что получим только ID материала.

// Теперь мы читаем имя материала, привязанного к обьекту
ReadObjectMaterial(pModel, pObject, m_CurrentChunk);
break;

case OBJECT_UV:     // Хранит текстурные координаты обьекта

// Эта секция содержит все UV-координаты обьекта. Прочитаем их.
ReadUVCoordinates(pObject, m_CurrentChunk);
break;

default:

// Read past the ignored or unknown chunks
// Читаем игнорируемые/неизвестные данные в «мусорный» массив
m_CurrentChunk->bytesRead += fread(gBuffer, 1, m_CurrentChunk->length
m_CurrentChunk->bytesRead, m_FilePointer);
break;
}

// Прибавляем прочитанные данные к счетчику
pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
}

delete m_CurrentChunk;
m_CurrentChunk = pPreviousChunk;
}

///////////////////////////////// PROCESS NEXT MATERIAL CHUNK «»»»»»»»»»\*
/////
/////   Эта функция хранит всю информацию о материале (текстуре)
/////
///////////////////////////////// PROCESS NEXT MATERIAL CHUNK «»»»»»»»»»\*

void CLoad3DS::ProcessNextMaterialChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
// Текущий chunk для работы
m_CurrentChunk = new tChunk;

// Продолжаем читать эти chunk-и, пока не дошли до конца подсекции
while (pPreviousChunk->bytesRead < pPreviousChunk->length)
{
// Читаем следующую секцию
ReadChunk(m_CurrentChunk);

// Проверяем, что именно мы прочитали
switch (m_CurrentChunk->ID)
{
case MATNAME:       // Эта секция хранит имя материала

// читаем имя материала
m_CurrentChunk->bytesRead +=
fread(pModel->pMaterials[pModel->numOfMaterials 1].strName,
1, m_CurrentChunk->length m_CurrentChunk->bytesRead, m_FilePointer);
break;

case MATDIFFUSE:        // Хранит RGB-цвет обьекта
ReadColorChunk(&(pModel->pMaterials[pModel->numOfMaterials 1]), m_CurrentChunk);
break;

case MATMAP:            // Это хидер информации о текстуре

// Читаем информацию информацию о материале
ProcessNextMaterialChunk(pModel, m_CurrentChunk);
break;

case MATMAPFILE:        // Хранит имя файла материала

// Читаем имя файла материала
m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials 1].strFile,
1, m_CurrentChunk->length m_CurrentChunk->bytesRead, m_FilePointer);
break;

default:

// Читаем остальные данные в «мусор»
m_CurrentChunk->bytesRead += fread(gBuffer, 1,
m_CurrentChunk->length m_CurrentChunk->bytesRead,
m_FilePointer);
break;
}

// Прибавляем прочитанные данные к счетчику
pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
}
delete m_CurrentChunk;
m_CurrentChunk = pPreviousChunk;
}

///////////////////////////////// READ CHUNK «»»»»»»»»»\*
/////
/////   Функция читает ID chunk-а и его длинну в байтах
/////
///////////////////////////////// READ CHUNK «»»»»»»»»»\*

void CLoad3DS::ReadChunk(tChunk *pChunk)
{
// Функция читает ID секции (2 байта).
// ID chunk-а — это, например, OBJECT/MATERIAL. Это говорит нам,
// какие данные могут быть прочитаны в этой секции.
pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);

// Затем читаем длинну секции (4 байта). Теперь мы знаем,
// сколько данных нам нужно будет прочитать.
pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
}

///////////////////////////////// GET STRING «»»»»»»»»»\*
/////
/////   Читает строку в массив char-ов
/////
///////////////////////////////// GET STRING «»»»»»»»»»\*

int CLoad3DS::GetString(char *pBuffer)
{
int index = 0;

// Читаем 1 байт данных, первую букву строки
fread(pBuffer, 1, 1, m_FilePointer);

// Цикл, пока не получаем NULL
while (*(pBuffer + index++) != 0) {

// Читаем символы всё время, пока не получим NULL
fread(pBuffer + index, 1, 1, m_FilePointer);
}

// Вернём длинну строки, т.е. сколько байтов мы прочитали (включая NULL)
return strlen(pBuffer) + 1;
}

///////////////////////////////// READ COLOR «»»»»»»»»»\*
/////
/////   Читает данные RGB-цвета
/////
///////////////////////////////// READ COLOR «»»»»»»»»»\*

void CLoad3DS::ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk)
{
// Читаем информацию о цвете
ReadChunk(m_TempChunk);

// Читаем RGB-цвет (3 байта — от 0 до 255)
m_TempChunk->bytesRead += fread(pMaterial->color, 1, m_TempChunk->length m_TempChunk->bytesRead, m_FilePointer);

// Увеличиваем счетчик
pChunk->bytesRead += m_TempChunk->bytesRead;
}

///////////////////////////////// READ VERTEX INDECES «»»»»»»»»»\*
/////
/////   Функция читает индексы для массива вершин
/////
///////////////////////////////// READ VERTEX INDECES «»»»»»»»»»\*

void CLoad3DS::ReadVertexIndices(t3DObject *pObject, tChunk *pPreviousChunk)
{
unsigned short index = 0;       // Используется для чтения индекса текущего полигона

// Чтобы прочитать индексы вершин для обьекта, нужно сначала прочитать их
// число, затем уже их самих. Запомните, нам нужно прочитать только 3 из
// 4 значений для каждого полигона. Это четвертое значение — флаг видимости
// для 3DS Max, которое ничего для нас не значит.

// Читаем число полигонов этого обьекта
pPreviousChunk->bytesRead += fread(&pObject->numOfFaces, 1, 2, m_FilePointer);

// Выделяем достаточно памяти для полигонов и инициализируем структуру
pObject->pFaces = new tFace [pObject->numOfFaces];
memset(pObject->pFaces, 0, sizeof(tFace) * pObject->numOfFaces);

// Проходим через все полигоны этого обьекта
for(int i = 0; i < pObject->numOfFaces; i++)
{
// Далее читаем A-B-C индексы для полигона, но игнорируем 4-е значение.
for(int j = 0; j < 4; j++)
{
// Читаем первый индекс вершины для текущего полигона
pPreviousChunk->bytesRead += fread(&index, 1, sizeof(index), m_FilePointer);

if(j < 3)
{
// Сохраняем индекс в структуру полигонов
pObject->pFaces[i].vertIndex[j] = index;
}
}
}
}

///////////////////////////////// READ UV COORDINATES «»»»»»»»»»\*
/////
/////   Функция читает UV-координаты обьекта
/////
///////////////////////////////// READ UV COORDINATES «»»»»»»»»»\*

void CLoad3DS::ReadUVCoordinates(t3DObject *pObject, tChunk *pPreviousChunk)
{
// Чтобы прочитать индексы UV-координат для обьекта, сначала нужно
// прочитать их полное количество, потом уже их самих.

// Читаем число UV-координат
pPreviousChunk->bytesRead += fread(&pObject->numTexVertex, 1, 2, m_FilePointer);

// Выделяем память для хранения UV-координат
pObject->pTexVerts = new CVector2 [pObject->numTexVertex];

// Читаем текстурные координаты (массив из 2х float)
pPreviousChunk->bytesRead += fread(pObject->pTexVerts, 1,
pPreviousChunk->length pPreviousChunk->bytesRead, m_FilePointer);
}

///////////////////////////////// READ VERTICES «»»»»»»»»»\*
/////
/////   Функция читает вершины обьекта
/////
///////////////////////////////// READ VERTICES «»»»»»»»»»\*

void CLoad3DS::ReadVertices(t3DObject *pObject, tChunk *pPreviousChunk)
{
// Как и в большинстве chunk-ов, прежде чем читать сами вершины,
// нужно найти их количество.

// Читаем число вершин
pPreviousChunk->bytesRead += fread(&(pObject->numOfVerts), 1, 2, m_FilePointer);

// Выделяем память для вершин и инициализируем структуру
pObject->pVerts = new CVector3 [pObject->numOfVerts];
memset(pObject->pVerts, 0, sizeof(CVector3) * pObject->numOfVerts);

// Читаем в массив вершин (массив из 3 float)
pPreviousChunk->bytesRead += fread(pObject->pVerts, 1,
pPreviousChunk->length pPreviousChunk->bytesRead, m_FilePointer);

// Теперь все вершины прочитаны. Так как в моделях 3DS Max всегда перевёрнуты
// оси, нужно поменять Y-значения и Z-значения наших вершин.

// Проходим через все вершины и меняем y<->z
for(int i = 0; i < pObject->numOfVerts; i++)
{
// Сохраняем старое знач-е Y
float fTempY = pObject->pVerts[i].y;

// Устанавливаем значение Y в Z
pObject->pVerts[i].y = pObject->pVerts[i].z;

// Устанавливаем значение Z в Y
// И делаем его отрицательным, т.к. в 3ds max Z-ось перевернута
pObject->pVerts[i].z = fTempY;
}
}

///////////////////////////////// READ OBJECT MATERIAL «»»»»»»»»»\*
/////
/////   Функция читает имя материала, наложенного на обьект, и устанавливает materialID
/////
///////////////////////////////// READ OBJECT MATERIAL «»»»»»»»»»\*

void CLoad3DS::ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
char strMaterial[255] = {0};        // Хранит имя материала

// *Что такое материал?* — Материал — это цвет + текстурная карта обьекта.
// Также он можетхранить другую информацию типа яркости, «блестящести» и т.д.
// Сейчас нам нужно только цвет или имя текстурной карты.

// Читаем имя материала, привязанного к текущему обьекту.
// strMaterial теперь должен содержать строку с именем материала, типа «Material #2» и т.д…
pPreviousChunk->bytesRead += GetString(strMaterial);

// Теперь, имея имя материала, нужно пройти через все материалы и проверять их
// имена на совпадение с нашим. Когда найдем материал с только что прочитанным
// именем, привязываем materialID обьекта к индексу этого материала.

// Проходим через все материалы
for(int i = 0; i < pModel->numOfMaterials; i++)
{
// Если только что прочитанный материал совпадает с именем данного
if(strcmp(strMaterial, pModel->pMaterials[i].strName) == 0)
{
// Проверяем, есть ли текстурная карта. Если strFile содержит
// строку с длинной >=1, текстура есть.
if(strlen(pModel->pMaterials[i].strFile) > 0)
{
// Устанавливаем ID материала в текущий индекс ‘i’ и заканчиваем проверку
pObject->materialID = i;

// Устанавливаем флаг текстурирования в true
pObject->bHasTexture = true;
}
break;
}
else
{
// Проверяем флаг, чтобы увидеть, есть ли уже текстура на этом обьекте
if(pObject->bHasTexture != true)
{
// Устанавливаем ID материала в -1, чтобы указать, что материала для обьекта нет
pObject->materialID = 1;
}
}
}

// Остальное читаем в «мусор»
pPreviousChunk->bytesRead += fread(gBuffer, 1,
pPreviousChunk->length pPreviousChunk->bytesRead, m_FilePointer);
}

// *Note*
//
// Ниже идут несколько математических функций, вычисляющих нормали вершин. Они нам
// нужны, чтобы эффект освещения рассчитывался верно. В прошлых уроках мы уже писали
// эти функции, так что при желании можно просто подключить файлы 3dmath.h/.cpp

//////////////////////////////  Math Functions  ////////////////////////////////*

// Рассчитывает величину нормали (magnitude = sqrt(x^2 + y^2 + z^2)
#define Mag(Normal) (sqrt(Normal.x*Normal.x + Normal.y*Normal.y + Normal.z*Normal.z))

// Рассчитывает векторы между 2 точками и возвращает результат
CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2)
{
CVector3 vVector;           // Хранит результирующий вектор

vVector.x = vPoint1.x vPoint2.x;
vVector.y = vPoint1.y vPoint2.y;
vVector.z = vPoint1.z vPoint2.z;

return vVector;             // Вернём результирующий вектор
}

// This adds 2 vectors together and returns the result
// Складывает 2 вектора и возвращает результат
CVector3 AddVector(CVector3 vVector1, CVector3 vVector2)
{
CVector3 vResult;               // Хранит результирующий вектор

vResult.x = vVector2.x + vVector1.x;
vResult.y = vVector2.y + vVector1.y;
vResult.z = vVector2.z + vVector1.z;

return vResult;                 // Вернём результат
}

// Делит вектор на переданный номер и возвращает результат
CVector3 DivideVectorByScaler(CVector3 vVector1, float Scaler)
{
CVector3 vResult;

vResult.x = vVector1.x / Scaler;
vResult.y = vVector1.y / Scaler;
vResult.z = vVector1.z / Scaler;

return vResult;
}

// Возвращает скалярное произведение (dot product) двух векторов
CVector3 Cross(CVector3 vVector1, CVector3 vVector2)
{
CVector3 vCross;

vCross.x = ((vVector1.y * vVector2.z) (vVector1.z * vVector2.y));
vCross.y = ((vVector1.z * vVector2.x) (vVector1.x * vVector2.z));
vCross.z = ((vVector1.x * vVector2.y) (vVector1.y * vVector2.x));

return vCross;
}

// Возвращает нормаль вектора
CVector3 Normalize(CVector3 vNormal)
{
double Magnitude;

Magnitude = Mag(vNormal);

vNormal.x /= (float)Magnitude;
vNormal.y /= (float)Magnitude;
vNormal.z /= (float)Magnitude;

return vNormal;
}

///////////////////////////////// COMPUTER NORMALS «»»»»»»»»»\*
/////
/////   Функция рассчитывает нормали для обьекта и его вершин
/////
///////////////////////////////// COMPUTER NORMALS «»»»»»»»»»\*

void CLoad3DS::ComputeNormals(t3DModel *pModel)
{
CVector3 vVector1, vVector2, vNormal, vPoly[3];

// Если обьектов нет, пропускаем этот шаг
if(pModel->numOfObjects <= 0)
return;

// Что такое нормали вершин? Чем они отличаются от остальных нормалей? Если вы
// нашли нормаль треугольника, это «нормаль полигона». Если вы передали OpenGL
// нормаль полигона для освещения, ваш обьект будет выглядеть плоским и резким.
// Если же вы найдете нормали для каждой вершины, освещенный обьект будет
// выглядеть сглаженным, т.е. более реалистичным.

// Проходим через все обьекты для вычисления их вершин
for(int index = 0; index < pModel->numOfObjects; index++)
{
// Получим текущий обьект
t3DObject *pObject = &(pModel->pObject[index]);

// Выделяем память под нужные переменные
CVector3 *pNormals      = new CVector3 [pObject->numOfFaces];
CVector3 *pTempNormals  = new CVector3 [pObject->numOfFaces];
pObject->pNormals       = new CVector3 [pObject->numOfVerts];

// Проходим через все полигоны обьекта
for(int i=0; i < pObject->numOfFaces; i++)
{
// Сохраняем 3 точки этого полигона, чтобы избежать большого кода
vPoly[0] = pObject->pVerts[pObject->pFaces[i].vertIndex[0]];
vPoly[1] = pObject->pVerts[pObject->pFaces[i].vertIndex[1]];
vPoly[2] = pObject->pVerts[pObject->pFaces[i].vertIndex[2]];

// Теперь вычислим нормали полигонов

vVector1 = Vector(vPoly[0], vPoly[2]);  // вектор полигона (из 2х его сторон)
vVector2 = Vector(vPoly[2], vPoly[1]);  // Второй вектор полигона

vNormal  = Cross(vVector1, vVector2);   // получаем cross product векторов
pTempNormals[i] = vNormal;      // временно сохраняем не-нормализированную нормаль
// для вершин
vNormal  = Normalize(vNormal);      // нормализируем cross product для нормалей полигона

pNormals[i] = vNormal;          // Сохраняем нормаль в массив
}

//////////////// Теперь получаем вершинные нормали /////////////////

CVector3 vSum = {0.0, 0.0, 0.0};
CVector3 vZero = vSum;
int shared=0;

for (int i = 0; i < pObject->numOfVerts; i++)   // Проходим через все вершины
{
for (int j = 0; j < pObject->numOfFaces; j++)   // Проходим через все треугольники
{               // Проверяем, используется ли вершина другим полигоном
if (pObject->pFaces[j].vertIndex[0] == i ||
pObject->pFaces[j].vertIndex[1] == i ||
pObject->pFaces[j].vertIndex[2] == i)
{
vSum = AddVector(vSum, pTempNormals[j]);    // Прибавляем не-
// нормализированную нормаль другого полигона
shared++;       // Увеличиваем число полигонов с общими вершиными
}
}

// Получаем нормаль делением на сумму общих полигонов. Делаем её отрицат.
pObject->pNormals[i] = DivideVectorByScaler(vSum, float(shared));

// Нормализуем нормаль для вершины
pObject->pNormals[i] = Normalize(pObject->pNormals[i]);

vSum = vZero;           // Сбрасываем сумму
shared = 0;         // И общие полигоны
}

// Освобождаем память временных переменных
delete [] pTempNormals;
delete [] pNormals;
}
}

 

Уфф! Сложновато. Теперь осталось только использовать написанный функционал.

Файл main.cpp:

// Изменим имя файла для загрузки:
#define FILE_NAME  «Character.3ds»///////////////////////////////// DRAW 3D GRID \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: рисуем простую сетку для лучшего восприятия сцены
/////
///////////////////////////////// DRAW 3D GRID \\\\\\\\\\\\\\\\*void Draw3DSGrid()
{
if(g_bLighting) glDisable(GL_LIGHTING);

glColor3ub(0, 255, 0);

// Сетка 10х10 вдоль осей X и Z
for(int x = 1000; x <= 1000; x += 10)
{
glBegin(GL_LINES);

// Горизонтальные линии (вдоль X)
glVertex3f(1000, 0, (GLfloat)x);
glVertex3f(1000, 0, (GLfloat)x);

// Вертикальные линии (вдоль Z)
glVertex3f((GLfloat)x, 0, 1000);
glVertex3f((GLfloat)x, 0, 1000);

glEnd();
}

glColor3ub(255, 255, 0);

glLineWidth(2.5);

glBegin(GL_LINES);

glVertex3f(1000, 0.01f, 0);
glVertex3f(1000, 0.01f, 0);

glVertex3f(0, 0.01f, 0);
glVertex3f(0, 1000, 0);

glVertex3f(0, 0.01f, 1000);
glVertex3f(0, 0.01f, 1000);

glEnd();

glColor3ub(255, 255, 255);

glLineWidth(1);

if(g_bLighting) glEnable(GL_LIGHTING);
}

///////////////////////////////// DELAY \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: возвращает true если переданное время прошло
/////
///////////////////////////////// DELAY \\\\\\\\\\\\\\\\*

bool Delay(int milliseconds)
{
// Получаем стартовое время, сохраняем эту статическую переменную только 1 раз
static float startTime = (float)GetTickCount();

// Получаем текущее время
float currentTime = (float)GetTickCount();

// Проверяем, больше ли прошедшее со старта время, чем переданное время
if( currentTime startTime > milliseconds )
{
// Устанавливаем новое начальное время в текущее время
startTime = currentTime;

// Вернём true, чтобы указать, что время прошло
return true;
}

// Переданное время ещё не прошло, так что вернём false
return false;
}

///////////////////////////////// ANIMATE MODEL \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: перемещает, масштабирует, вращает обьект каждый кадр
/////
///////////////////////////////// ANIMATE MODEL \\\\\\\\\\\\\\\\*

void AnimateModel(t3DModel *pModel, t3DObject *pObject)
{
// Это самое главное изменение в этом файле. Эта функция контролирует
// изменения обьекта для каждого кадра анимации. В зависимости от кадра
// мы перемещаем, затем масштабируем, затем вращаем обьект на необходимые
// значения. Для каждого обьекта есть список значений для каждого кадра.

// Получаем текущую позицию для этого кадра
CVector3 vPosition = pObject->vPosition[pModel->currentFrame];

// Перемещаем обьект
glTranslatef(vPosition.x, vPosition.y, vPosition.z);

// Получаем значение scale для этого кадра
CVector3 vScale = pObject->vScale[pModel->currentFrame];

// Масштабируем обьект на значение для этого кадра
glScalef(vScale.x, vScale.y, vScale.z);

// Здесь всё немного по другому. Создаём цикл через все
// предыдущие кадры, пока не достигнем первого кадра, и
// вызываем для каждого кадра glRotatef(). Это будет вращать
// обьект на маленькие углы, пока не получится значение текущего
// кадра. Если бы мы использовали матрицы (но я решил не перегружать
// этот урок), можно было бы просто умножить матрицу вращения
// на текущую матрицу.
// Устанавливаем «i» в 1, т.к. мы игнорируем нулевой кадр анимации.
for(int i = 1; i <= pModel->currentFrame; i++)
{
// Получаем текущий угол и ось вращения для текущего кадра
CVector3 vRotation = pObject->vRotation[i];
float rotDegree = pObject->vRotDegree[i];

// Проверяем, не равен ли угол вращения нулю
if(rotDegree)
{
// Вращаем обьект на угол для текущего кадра
glRotatef(rotDegree, vRotation.x, vRotation.y, vRotation.z);
}
}
}

/////////////////////////////////////////////////////////////////////////////////////////
//
// Изменяем функцию RenderScene().
//

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt(0, 35, 75,        0, 25, 0,           0, 1, 0);

// Мы хотим, чтобы модель вращалась вокруг своей оси, так что передадим переменную
// вращения.

glRotatef(g_RotateX, 0, 1.0f, 0);   // Вращаем обьект по Y-оси
g_RotateX += g_RotationSpeed;       // Увеличиваем скорость вращения

// Рисуем простую сетку
Draw3DSGrid();

if(Delay(16))
{
g_3DModel.currentFrame = (g_3DModel.currentFrame) % (g_3DModel.numberOfFrames) + 1;
}

// Так как мы знаем число обьектов в нашей модели, проходим через каждый из них
for(int i = 0; i < g_3DModel.numOfObjects; i++)
{
// Убедимся, что передан верный обьект
if(g_3DModel.pObject.size() <= 0) break;

// Получим текущий обьект
t3DObject *pObject = &g_3DModel.pObject[i];

glPushMatrix();

// Анимируем модель
AnimateModel(&g_3DModel, pObject);

// Проверим, имеет ли обьект тексурную карту, если да — биндим на него текстуру
if(pObject->bHasTexture) {

// Включаем текстуры
glEnable(GL_TEXTURE_2D);

// Сбрасываем цвет
glColor3ub(255, 255, 255);

// Биндим текстурную карту на обьект по его materialID
glBindTexture(GL_TEXTURE_2D, textures[pObject->materialID].texID);
} else {

// Иначе выключим текстуры
glDisable(GL_TEXTURE_2D);

// И сбросим цвет на нормальный
glColor3ub(255, 255, 255);
}

// Начинаем отрисовку в выбранном режиме
glBegin(g_ViewMode);    // Рисуем обьекты (треугольники или линии)

// Проходим через все полигоны обьекта и рисуем их
for(int j = 0; j < pObject->numOfFaces; j++)
{
// Проходим через каждый угол треугольника и рисуем его
for(int whichVertex = 0; whichVertex < 3; whichVertex++)
{
// Get the index for each point of the face
// Получаем индекс для каждой точки полигона
int index = pObject->pFaces[j].vertIndex[whichVertex];

// Передаём OpenGL нормаль этой вершины
glNormal3f(pObject->pNormals[ index ].x,
pObject->pNormals[ index ].y, pObject->pNormals[ index ].z);

// Если обьект имеет текстуру, передаем текст. координаты
if(pObject->bHasTexture) {

// Убедимся, что UVW-мап применена на обьект, иначе
// он не будет иметь текстурных координат
if(pObject->pTexVerts) {
glTexCoord2f(pObject->pTexVerts[ index ].x, pObject->pTexVerts[ index ].y);
}
} else {

// Убедимся, что у нас есть верный материал/цвет, привязанный
// к обьекту. Вообще практически всегда к обьекту привязан как
// минимум цвет, но просто на всякий случай проверим это.
// Если размер материала минимум 1, и materialID != -1,
// материал верен.
if(g_3DModel.pMaterials.size() && pObject->materialID >= 0)
{
// Получаем и устанавливаем цвет обьекта, если он
// не имеет текстуры
BYTE *pColor = g_3DModel.pMaterials[pObject->materialID].color;

// Применяем цвет к модели
glColor3ub(pColor[0], pColor[1], pColor[2]);
}
}

// Передаём текущую вершину обьекта
glVertex3f(pObject->pVerts[ index ].x, pObject->pVerts[ index ].y,
pObject->pVerts[ index ].z);
}
}

glEnd();
glPopMatrix();
}

SwapBuffers(g_hDC);
}

////////////////////////////////
//
// Далее после обьявления обьекта «t3DObject *pObject = &g_3DModel.pObject[i];» пишем:

// Входим в новую матрицу
glPushMatrix();

// Анимируем модель
AnimateModel(&g_3DModel, pObject);

///////////////////////////////
//
// И в конце цикла выходим из матрицы:
//

glPopMatrix();

////////////////////////////////////////////////////////////////////////////////////////////////

Исходные коды к уроку