Этот урок продемонстрирует способ загрузки файлов .3ds. Этот формат можно создать, например, в 3d Studio Max, а также может импортироваться и экспортироваться многими другими программами.
Отличная утилита для конвертирования 3D-форматов — «3D Exploration». Это shareware-программа, которую вы можете найти в интернете.
Наш загрузчик 3DS сможет загружать только имена текстур, цвета обьектов, вершины, полигоны, и текстурные координаты. Информация о кадрах игнорируется, так как пока что у нас не будет покадровой анимации.
В уроке будет вращающаяся 3D-модель лица с текстурой. Клавиши управления:
ЛКМ: изменение режима рендера с нормального на каркасный и наоборот.
ПКМ: Вкл/Выкл освещение.
ВЛЕВО: Увелич. скорость вращения влево
ВПРАВО: Увелич. скорость вращения вправо
Исходный код взят из урока «загрузка текстур», добавлены два новых файла: 3ds.h и 3ds.cpp.
Файл main.h:
// загружать и анимацию, нужно будет кое-что сюда добавить.// Сначала включим новые хидеры:
#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:
#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 «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(¤tChunk);
// Убедимся, что это 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, ¤tChunk);
// После прочтения всего файла нам нужно рассчитать нормали вершин
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(¤tChunk);
// Получаем 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, ¤tChunk);
break;
}
case MATERIAL: // Содержит информацию о материале
// Этот chunk — хидер для информации о материале
// Увеличиваем число материалов
pModel->numOfMaterials++;
// Добавляем пустую структуру текстуры в наш массив текстур.
pModel->pMaterials.push_back(newTexture);
// Вызываем функцию, обрабатывающую материал
ProcessNextMaterialChunk(pModel, ¤tChunk);
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]), ¤tChunk);
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(¤tChunk);
// Проверяем, что это за секция
switch (currentChunk.ID)
{
case OBJECT_MESH: // Даёт нам знать, что мы читаем новый обьект
// Нашли новый обьект, прочитаем его информацию рекурсией
ProcessNextObjectChunk(pModel, pObject, ¤tChunk);
break;
case OBJECT_VERTICES: // Вершины нашего обьекта
ReadVertices(pObject, ¤tChunk);
break;
case OBJECT_FACES: // Полигоны обьекта
ReadVertexIndices(pObject, ¤tChunk);
break;
case OBJECT_MATERIAL: // Имя материала обьекта
// Эта секция хранит имя материала, связанного с текущим обьектом. Это может быть
// цвет или текстурная карта. Эта секция также содержит полигоны, к которым
// привязана текстура (Если например на обьекте несколько текстур, или просто
// текстура наложено только на часть обьекта). Сейчас у нас будет только одна
// текстура на весь обьект, так что получим только ID материала.
// Теперь мы читаем имя материала, привязанного к обьекту
ReadObjectMaterial(pModel, pObject, ¤tChunk);
break;
case OBJECT_UV: // Хранит текстурные координаты обьекта
// Эта секция содержит все UV-координаты обьекта. Прочитаем их.
ReadUVCoordinates(pObject, ¤tChunk);
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(¤tChunk);
// Проверяем, что именно мы прочитали
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]), ¤tChunk);
break;
case MATMAP: // Это хидер информации о текстуре
// Читаем информацию информацию о материале
ProcessNextMaterialChunk(pModel, ¤tChunk);
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;