Этот урок продемонстрирует способ загрузки файлов .3ds. Этот формат можно создать, например, в 3d Studio Max, а также может импортироваться и экспортироваться многими другими программами.
Отличная утилита для конвертирования 3D-форматов — «3D Exploration». Это shareware-программа, которую вы можете найти в интернете.
Наш загрузчик 3DS сможет загружать только имена текстур, цвета обьектов, вершины, полигоны, и текстурные координаты. Информация о кадрах игнорируется, так как пока что у нас не будет покадровой анимации.

В уроке будет вращающаяся 3D-модель лица с текстурой. Клавиши управления:

ЛКМ: изменение режима рендера с нормального на каркасный и наоборот.
ПКМ: Вкл/Выкл освещение.
ВЛЕВО: Увелич. скорость вращения влево
ВПРАВО: Увелич. скорость вращения вправо

Исходный код взят из урока «загрузка текстур», добавлены два новых файла: 3ds.h и 3ds.cpp.

Файл main.h:

// Этот файл включает все структуры, необходимые нам для загрузки 3ds-файла. Если вы решите
// загружать и анимацию, нужно будет кое-что сюда добавить.// Сначала включим новые хидеры:
#include <vector>
#include <math.h>
// Обьявим пространство имён:
using namespace std;// Структура 3D-точки
struct CVector3{
float x,y,z;
};// Структура 2D-точки
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;              // Полигоны обьекта
};

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

 

Что такое STL (Standard Template Library) Vector?

Давайте я коротко объясню, что такое векторы, если вы этого не знаете.
Чтобы использовать векторы, сначала нужно включить в программу и использовать пространство имён: using namespace std;
Вектор — это базирующийся на массивах список ссылок. Он позволяет динамически добавлять или удалять элементы. По сути это темплейт-класс, так что могут быть использованы списки ЛЮБОГО типа. Чтобы создать вектор с типом «int», нужно ввести: vector myIntList;

Теперь вы можете добавлять int-ы в динамический массив, говоря: myIntList.push_back(10);
Или: myIntList.push_back(num);. Чем больше вы добавляете элементов, тем больше становится ваш массив. С векторами вы можете использовать индексы так же, как и с обычными массивами: myIntList[0] = 0; Чтобы удалить элемент, используется функция pop_back(). Чтобы очистить вектор, используется clear(). Но это очистит список не во всех случаях, например не в случае, если у вас есть структуры данных, требующие очистки «изнутри», как например наш обьект.

Теперь создадим два новых файла, описывающих загрузку .3ds

Файл 3ds.h:

#ifndef _3DS_H
#define _3DS_H//>—— Главный Chunk, в начале каждого 3ds-файла
#define PRIMARY       0x4D4D//>—— Главнык Chunk-и
#define OBJECTINFO    0x3D3D            // Это предоставляет версию меша перед информацией об обьекте
#define VERSION       0x0002            // Предоставляет версию .3ds файла
#define EDITKEYFRAME  0xB000            // Хидер для всей информации о кадрах//>—— под-дефайны OBJECTINFO
#define MATERIAL      0xAFFF        // Информация о текстурах
#define OBJECT        0x4000        // Полигоны, вершины, и т.д…//>—— под-дефайны для MATERIAL
#define MATNAME       0xA000            // Название материала
#define MATDIFFUSE    0xA020            // Хранит цвет обьекта/материала
#define MATMAP        0xA200            // Хидер для нового материала
#define MATMAPFILE    0xA300            // Хранит имя файла текстуры#define OBJECT_MESH   0x4100            // Даёт нам знать, что начинаем считывать новый обьект//>—— под-дефайны для OBJECT_MESH
#define OBJECT_VERTICES     0x4110      // Вершины обьекта
#define OBJECT_FACES        0x4120      // Полигоны обьекта
#define OBJECT_MATERIAL     0x4130      // Дефайн находится, если обьект имеет материал, иначе цвет/текстура
#define OBJECT_UV       0x4140      // UV текстурные координаты

// Структура для индексов 3DS (так как .3ds хранит 4 unsigned short)
struct tIndices {
unsigned short a, b, c, bVisible;   // Это хранит индексы для точки 1,2,3 массива
// вершин, плюс флаг видимости
};

// Хранит информацию о chunk-е
struct tChunk
{
unsigned short int ID;          // ID chunk-а
unsigned int length;            // Длинна chunk-а
unsigned int bytesRead;         // Число читаемых байт для этого chunk-а
};

// Класс содержит весь код загрузки
class CLoad3DS
{
public:
CLoad3DS();     // Тут будут инициализироватся все данные

// Эта ф-я и будет вызыватся для загрузки 3DS
bool Import3DS(t3DModel *pModel, char *strFileName);

private:
// Читает строку и сохраняет её в переданный массив char-ов
int GetString(char *);

// Читает следующий chunk
void ReadChunk(tChunk *);

// Читает следующий длинный chunk
void ProcessNextChunk(t3DModel *pModel, tChunk *);

// Читает chunk-и обьекта
void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *);

// Читает chunk-и материала
void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *);

// Читает RGB-значение цвета обьекта
void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk);

// Читает вершины обьекта
void ReadVertices(t3DObject *pObject, tChunk *);

// Читает информацию полигонов обьекта
void ReadVertexIndices(t3DObject *pObject, tChunk *);

// Читает текстурные координаты обьекта
void ReadUVCoordinates(t3DObject *pObject, tChunk *);

// Читает имя материала, присвоенного обьекту, и устанавливает materialID
void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk);

// Рассчитывает нормали вершин обьекта
void ComputeNormals(t3DModel *pModel);

// This frees memory and closes the file
// Освобождает память и закрывает файл
void CleanUp();

// Указатель на файл
FILE *m_FilePointer;
};

#endif

 

Файл 3ds.cpp:

#include «main.h»
#include «3ds.h»int gBuffer[50000] = {0};   // Используется для чтения нежелательных данных// В этом файле находится весь код, необходимый для загрузки файлов .3ds.
// Как оно работает: вы загружаете chunk, затем проверяете его ID.
// В зависимости от его ID, загружаете информацию, хранящуюся в chunk-е.
// Если вы не хотите читать эту информацию, читаете дальше неё.
// Вы знаете, как много байт нужно пропустить, так как каждый chunk хранит
// свою длинну в байтах.///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\*
/////
/////   Конструктор инициализирует данные tChunk
/////
///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\*CLoad3DS::CLoad3DS()
{
m_FilePointer = NULL;
}///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\*
/////
/////   Вызывается клиентом для открытия, чтения и затем очистки .3ds
/////
///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\*bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName)
{
char strMessage[255] = {0};
tChunk currentChunk = {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(&currentChunk);

// Убедимся, что это 3DS
if (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, &currentChunk);

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

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

return true;
}

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

void CLoad3DS::CleanUp()
{
if (m_FilePointer) {
fclose(m_FilePointer);  // Закрываем файл
m_FilePointer = NULL;
}
}

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

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

tChunk currentChunk = {0};      // Текущий chunk для загрузки
tChunk tempChunk = {0};         // Временный chunk для хранения данных

// Ниже проверяем ID chunk-a каждый раз при чтении нового. Затем,
// если нужно вытащить данные из chunk-а, делаем это. Если же этот chunk нам
// не нужен, просто читаем chunk в «мусорный» массив.

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

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

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

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

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

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

// Получаем версию меша
tempChunk.bytesRead += fread(gBuffer, 1, tempChunk.length
tempChunk.bytesRead, m_FilePointer);

// Увеличиваем bytesRead на число прочитанных байт
currentChunk.bytesRead += tempChunk.bytesRead;

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

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

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

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

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

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

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

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

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

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

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

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

case EDITKEYFRAME:

// Так как я хотел сделать ПРОСТОЙ урок, насколько это возможно, я не включил
// информацию о покадровой анимации. Этот chunk — хидер для всей информации
// об анимации. В будущих уроках этот аспект будет детально описан.

//ProcessNextKeyFrameChunk(pModel, currentChunk);

// Читаем в «мусорный» контейнер ненужные данные и увеличиваем счетчик
currentChunk.bytesRead += fread(gBuffer, 1, currentChunk.length
currentChunk.bytesRead, m_FilePointer);
break;

default:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

default:

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

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

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

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

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

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

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

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

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

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

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

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

default:

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

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

///////////////////////////////// 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)
{
tChunk tempChunk = {0};

// Читаем информацию о цвете
ReadChunk(&tempChunk);

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

// Увеличиваем счетчик
pChunk->bytesRead += 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;
}
}

 

Это был БОЛЬШОЙ обьем знаний, и, наверно, пока что самый большой урок!
В следующих уроках мы узнаем, как загружать текстовые файлы .obj
Это самый распространённый 3D-формат, который понимает почти ВЕСЬ софт.

Ещё раз обращаю ваше внимание на то, что системы координат 3DS Max и OpenGL — разные.
Посколько ось Z в 3DS расположена вертикально, мы должны поменять местами значения Y и Z. Такэе поскольку мы поменяли местами Y и Z, нужно сделать значение Z отрицательным, чтобы модель была корректной.

CHUNK: Что это такое?

chunk ID — уникальный код, идентифицирующий тип данных в этом chunk-е. Длинна chunk-а показывает длинну последующих данных, относящихся к этому chunk-у. Запомните, чтобы пропустить ненужные данные раздела, нужно прочитать в мусорную переменную число байт ненужных данных.

В двух словах, обьявление chunk-а выглядит так:
2 байта — хранит ID раздела (OBJECT, MATERIAL, PRIMARY, и т.д…)
4 байта — хранит длинну этого раздела. С её помощью вы определяете, прочитан ли уже этот раздел.

Таким образом, чтобы начать чтение 3DS файла,вы читаете первые 2 байта, затем длинну.
Первые 2 байта должны быть chunk-ом PRIMARY, иначе это не 3ds-файл.

Ниже — список порядка, в котором расположены разделы в файле .3ds
Для большей информации об этом формате, прочитайте 3ds_format.rtf

// MAIN3DS (0x4D4D)
// |
// +—EDIT3DS (0x3D3D)
// | |
// | +—EDIT_MATERIAL (0xAFFF)
// | | |
// | | +—MAT_NAME01 (0xA000) (See mli Doc)
// | |
// | +—EDIT_CONFIG1 (0x0100)
// | +—EDIT_CONFIG2 (0x3E3D)
// | +—EDIT_VIEW_P1 (0x7012)
// | | |
// | | +—TOP (0x0001)
// | | +—BOTTOM (0x0002)
// | | +—LEFT (0x0003)
// | | +—RIGHT (0x0004)
// | | +—FRONT (0x0005)
// | | +—BACK (0x0006)
// | | +—USER (0x0007)
// | | +—CAMERA (0xFFFF)
// | | +—LIGHT (0x0009)
// | | +—DISABLED (0x0010)
// | | +—BOGUS (0x0011)
// | |
// | +—EDIT_VIEW_P2 (0x7011)
// | | |
// | | +—TOP (0x0001)
// | | +—BOTTOM (0x0002)
// | | +—LEFT (0x0003)
// | | +—RIGHT (0x0004)
// | | +—FRONT (0x0005)
// | | +—BACK (0x0006)
// | | +—USER (0x0007)
// | | +—CAMERA (0xFFFF)
// | | +—LIGHT (0x0009)
// | | +—DISABLED (0x0010)
// | | +—BOGUS (0x0011)
// | |
// | +—EDIT_VIEW_P3 (0x7020)
// | +—EDIT_VIEW1 (0x7001)
// | +—EDIT_BACKGR (0x1200)
// | +—EDIT_AMBIENT (0x2100)
// | +—EDIT_OBJECT (0x4000)
// | | |
// | | +—OBJ_TRIMESH (0x4100)
// | | | |
// | | | +—TRI_VERTEXL (0x4110)
// | | | +—TRI_VERTEXOPTIONS (0x4111)
// | | | +—TRI_MAPPINGCOORS (0x4140)
// | | | +—TRI_MAPPINGSTANDARD (0x4170)
// | | | +—TRI_FACEL1 (0x4120)
// | | | | |
// | | | | +—TRI_SMOOTH (0x4150)
// | | | | +—TRI_MATERIAL (0x4130)
// | | | |
// | | | +—TRI_LOCAL (0x4160)
// | | | +—TRI_VISIBLE (0x4165)
// | | |
// | | +—OBJ_LIGHT (0x4600)
// | | | |
// | | | +—LIT_OFF (0x4620)
// | | | +—LIT_SPOT (0x4610)
// | | | +—LIT_UNKNWN01 (0x465A)
// | | |
// | | +—OBJ_CAMERA (0x4700)
// | | | |
// | | | +—CAM_UNKNWN01 (0x4710)
// | | | +—CAM_UNKNWN02 (0x4720)
// | | |
// | | +—OBJ_UNKNWN01 (0x4710)
// | | +—OBJ_UNKNWN02 (0x4720)
// | |
// | +—EDIT_UNKNW01 (0x1100)
// | +—EDIT_UNKNW02 (0x1201)
// | +—EDIT_UNKNW03 (0x1300)
// | +—EDIT_UNKNW04 (0x1400)
// | +—EDIT_UNKNW05 (0x1420)
// | +—EDIT_UNKNW06 (0x1450)
// | +—EDIT_UNKNW07 (0x1500)
// | +—EDIT_UNKNW08 (0x2200)
// | +—EDIT_UNKNW09 (0x2201)
// | +—EDIT_UNKNW10 (0x2210)
// | +—EDIT_UNKNW11 (0x2300)
// | +—EDIT_UNKNW12 (0x2302)
// | +—EDIT_UNKNW13 (0x2000)
// | +—EDIT_UNKNW14 (0xAFFF)
// |
// +—KEYF3DS (0xB000)
// |
// +—KEYF_UNKNWN01 (0xB00A)
// +—…………. (0x7001) ( viewport, same as editor )
// +—KEYF_FRAMES (0xB008)
// +—KEYF_UNKNWN02 (0xB009)
// +—KEYF_OBJDES (0xB002)
// |
// +—KEYF_OBJHIERARCH (0xB010)
// +—KEYF_OBJDUMMYNAME (0xB011)
// +—KEYF_OBJUNKNWN01 (0xB013)
// +—KEYF_OBJUNKNWN02 (0xB014)
// +—KEYF_OBJUNKNWN03 (0xB015)
// +—KEYF_OBJPIVOT (0xB020)
// +—KEYF_OBJUNKNWN04 (0xB021)
// +—KEYF_OBJUNKNWN05 (0xB022)

Зная, как читать chunk-и, всё, что вам нужно знать — ID нужного раздела.
В этом уроке было очень уж много информации для одного раза. В следующем уроке я в основном просто более подробно обьясню то, что мы сделали в этом.

Файл main.cpp:

// В начале файла добавим новый инклуд и новые переменные:
#include «3ds.h»// Имя файла для загрузки:
#define FILE_NAME  «face.3ds»TextureImage Textures[100];         // текстурыCLoad3DS g_Load3ds;             // Наш 3DS класс.
t3DModel g_3DModel;             // Хранит загруженную 3D-модельint   g_ViewMode    = GL_TRIANGLES;     // По умолчанию режим рендера — GL_TRIANGLES
bool  g_bLighting   = true;         // Триггер освещения
float g_RotateX     = 0.0f;         // Угол вращения модели
float g_RotationSpeed   = 0.8f;         // Скорость вращения//////////////////////////////////////////////////////////////////////////////////////////////
//
// Модифицируем функцию init():
// // Сначала загружаем наш .3ds файл. Передаём указатель на структуру и имя файла.
g_Load3ds.Import3DS(&g_3DModel, FILE_NAME);

// В зависимости от числа найденных текстур, загружаем каждую (Подразумевается .BMP)
// Ниже мы проходим через все материалы и проверяем, имеют ли они текстуры.
// Иначе материал содержит только информацию о цвете.

// Проходим через все материалы
for(int i = 0; i < g_3DModel.numOfMaterials; i++)
{
// Проверяем, есть ли в загруженном материале имя файла
if(strlen(g_3DModel.pMaterials[i].strFile) > 0)
{
// Используем имя файла для загрузки битмапа с текстурным ID (i).
Texture->LoadTexture(IL_BMP, g_3DModel.pMaterials[i].strFile, &textures[i]);
}

// Устанавливаем ID текстуры для этого материала
g_3DModel.pMaterials[i].texureId = i;
}

// Включаем освещение и цветные материалы.

glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
glEnable(GL_COLOR_MATERIAL);

///////////////////////////////////////////////////////////////////////////////////////////
//
// Добавим в функцию DeInit():
//
for(int i = 0; i < g_3DModel.numOfObjects; i++)
{
// Очищаем структуры
delete [] g_3DModel.pObject[i].pFaces;
delete [] g_3DModel.pObject[i].pNormals;
delete [] g_3DModel.pObject[i].pVerts;
delete [] g_3DModel.pObject[i].pTexVerts;
}

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

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt(0, 1.5f, 8,0, 0.5f, 0,0, 1, 0);

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

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

// Я попытаюсь обьяснить, что происходит дальше. У нас есть модель с некоторым
// количеством обьектов и текстур. Мы хотим пройти через все обьекты модели, биндя
// на них текстуры, и рендерить их.
// Чтобы отрендерить текущий обьект, проходим через все его полигоны. Полигон — это
// просто один из (в данном случае) треугольников обьекта. Например, в кубе есть 12
// полигонов, так как каждая из его 6 сторон содержит 2 треугольника. Вы можете подумать,
// что если в обьекте 12 треугольника, то в нём 36 вершин. На самом деле это не так.
// Так как многие вершины одинаковы, потому что используются разными сторонами куба,
// нужно сохранять только 8 вершин, и игнорировать дублированные. Таким образом
// у вас будет массив уникальных вершин обьекта, что сбережет большое число
// памяти. После этого сохраняется массив индексов вершин для каждого полигона,
// каждый индекс указывает на вершину в массиве вершин. Это может показатся громоздким,
// но на самом деле это гораздо лучше, чем сохранять дублирующиеся вершины. То же
// самое касается текстурных UV-координат. Вам не нужно сохранять дублирующиеся UV
// координаты: сохраняете только уникальные, а затем создаёте массив индексов.
// Это может смущать, но большинство форматов 3D-файлов так и делают.
// Цикл ниже будет оставатся неизменным для большинства форматов, всё, что вам
// нужно будет изменить — код загрузки. (это не касается анимации)

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

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

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

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

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

// Биндим текстурную карту на обьект по его materialID
glBindTexture(GL_TEXTURE_2D, textures[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();
}

SwapBuffers(g_hDC);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Изменим блок обработки клавиш в функции WinProc().
// Клавиши управления:
//
// Left Mouse Button — Изменяет режим рендера с нормального на сетку
// Right Mouse Button — Вкл/Выкл освещение
// Left Arrow Key — Увелич. вращение влево
// Right Arrow Key — Увелич. вращение вправо
// Escape — Выход

case WM_LBUTTONDOWN:

if(g_ViewMode == GL_TRIANGLES) {
g_ViewMode = GL_LINE_STRIP;
} else {
g_ViewMode = GL_TRIANGLES;
}
break;

case WM_RBUTTONDOWN:

g_bLighting = !g_bLighting;

if(g_bLighting) {
glEnable(GL_LIGHTING);
} else {
glDisable(GL_LIGHTING);
}
break;

case WM_KEYDOWN:

switch(wParam) {
case VK_ESCAPE:
PostQuitMessage(0);
break;

case VK_LEFT:
g_RotationSpeed -= 0.05f;
break;

case VK_RIGHT:
g_RotationSpeed += 0.05f;
break;
}
break;

 

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