Этот урок показывает способ загрузки моделей из файлов .obj. Формат obj — это текстовые файлы, содержащие очень мало данных о модели. Но благодаря своей простоте эти файлы может импортировать/экспортировать практически любой соответствующий софт.
Код урока взят из урока «загрузка текстур».
Минус формата .obj — он не хранит информации о материалах, так что мне пришлось придумать другой способ.
Я бы не стал использовать этот формат для демки или игры, но полезно уметь с ним обращатся, так как он очень прост и популярен. Вообще, надо было написать этот урок раньше загрузки 3sd…
В этом уроке мы будем использовать STL vector. Что это такое я подробно описал в уроке «загрузка .3ds».
Вот небольшое описание формата OBJ. Каждая программа, экспортирующая и импортирующая этот формат, делает это по-своему. Некоторые сохраняют нормали, некоторые — имена обьектов, и так далее.
Данные вы читаете на основе первого символа в строке.
«v» — эта линия содержит вершину (x y z)
// пример: v -1 -1 0
Прочитав все такие линии, вы получите геометрию обьекта.
«vt» — текстурные координаты обьекта (U V)
// пример: vt .99998 .99936
«f» — эта линия содержит индексы вершин для массива полигонов.
Если есть UV координаты, она содержит также и их индексы.
// пример: (Только вершины): f 1 2 3
// пример: (Вершины и текстуры): f 1/1 2/2 3/3
Теперь напишем класс, загружающий .obj-файл.
Новый файл: obj.h:
#define _OBJ_H#include «main.h»// Класс, загружающий файл формата OBJ
class CLoadObj {
public:
// Вы будете вызывать только эту функцию. Просто передаёте структуру
// модели для сохранения данных, и имя файла для загрузки.
bool ImportObj(t3DModel *pModel, char *strFileName);
// Главный загружающий цикл, вызывающийся из ImportObj()
void ReadObjFile(t3DModel *pModel);
// Вызывается в ReadObjFile() если линия начинается с ‘v’
void ReadVertexInfo();
// Вызывается в ReadObjFile() если линия начинается с ‘f’
void ReadFaceInfo();
// Вызывается после загрузки информации полигонов
void FillInObjectInfo(t3DModel *pModel);
// Вычисление нормалей. Это не обязятельно, но очень желательно.
void ComputeNormals(t3DModel *pModel);
// Так как .obj файлы не хранят имен текстур и информации о материалах, мы создадим
// функцию, устанавливающую их вручную. materialID — индекс для массива pMaterial нашей модели.
void SetObjectMaterial(t3DModel *pModel, int whichObject, int materialID);
// Чтобы проще присваивать материал к .obj обьекту, создадим для этого функцию.
// Передаём в неё модель, имя материала, имя файла текстуры и цвет RGB.
// Если нам нужен только цвет, передаём NULL для strFile.
void AddMaterial(t3DModel *pModel, char *strName, char *strFile,
int r = 255, int g = 255, int b = 255);
private:
// Указатель на файл
FILE *m_FilePointer;
// STL vector, содержащий список вершин
vector<CVector3> m_pVertices;
// STL vector, содержащий список полигонов
vector<tFace> m_pFaces;
// STL vector, содержащий список UV координат
vector<CVector2> m_pTextureCoords;
// Говорит нам, имеет ли обьект текстурные координаты
bool m_bObjectHasUV;
// Говорит нам, что мы только что прочитали данные полигонов, чтобы мы могли читать несколько обьектов
bool m_bJustReadAFace;
};
#endif
Файл obj.cpp:
#include «obj.h»
///////////////////////////////// IMPORT OBJ \\\\\\\\\\\\\\\\*
/////
///// Функция загружает файл .obj в указанную переменную из указанного имени файла
/////
///////////////////////////////// IMPORT OBJ \\\\\\\\\\\\\\\\*
bool CLoadObj::ImportObj(t3DModel *pModel, char *strFileName)
{
char strMessage[255] = {0}; // Будет использоваться для сообщения об ошибке
// Убедимся, что переданы не пустые модель и имя файла
if(!pModel || !strFileName) return false;
// Открываем указанный файл для чтения
m_FilePointer = fopen(strFileName, «r»);
// Убедимся, что файл правильно открыт
if(!m_FilePointer) {
// Сформируем сообщение об ошибке
sprintf(strMessage, «Unable to find or open the file: %s», strFileName);
MessageBox(NULL, strMessage, «Error», MB_OK);
return false;
}
// Теперь, имея открытый файл, считываем информацию
ReadObjFile(pModel);
// Прочитав всю информацию, вычисляем вершинные нормали
ComputeNormals(pModel);
// Закрываем файл
fclose(m_FilePointer);
// И возвращаем true
return true;
}
///////////////////////////////// READ OBJ FILE \\\\\\\\\\\\\\\\*
/////
///// Эта функция — главный цикл чтения файла .obj
/////
///////////////////////////////// READ OBJ FILE \\\\\\\\\\\\\\\\*
void CLoadObj::ReadObjFile(t3DModel *pModel)
{
char strLine[255] = {0};
char ch = 0;
while(!feof(m_FilePointer))
{
float x = 0.0f, y = 0.0f, z = 0.0f;
// Читаем первый символ текущей строки файла
ch = fgetc(m_FilePointer);
switch(ch)
{
case ‘v’: // Проверяем, не ‘v’ ли это (может быть вершина/нормаль/текст. коорд.)
// Если мы только что читали информацию о полигоне, а сейчас читаем вершину,
// значит мы перешли к следующему обьекту, и нужно сохранить данные предыдущего.
if(m_bJustReadAFace) {
// Сохраняем данные последнего обьекта в структуру модели
FillInObjectInfo(pModel);
}
// Расшифровываем всю текущую линию
ReadVertexInfo();
break;
case ‘f’: // Если первый символ -‘f’, эта строка описывает полигон
// Нужно считать информацию о полигоне. Строки полигонов могут
// содержать или индексы вершин, или ещё и текстурные координаты,
// если они есть у обьекта.
ReadFaceInfo();
break;
case ‘n’:
// Если прочитан символ новой строки — это пустая строка, ничего не делаем.
break;
default:
// Если что-то неизвестное, просто читаем эту строку в «мусор», чтобы перейти
// к следующей — нам она не нужна.
fgets(strLine, 100, m_FilePointer);
break;
}
}
// Теперь сохраняем последний прочитанный обьект
FillInObjectInfo(pModel);
}
///////////////////////////////// READ VERTEX INFO \\\\\\\\\\\\\\\\*
/////
///// Эта функция читает информацию о вершинах («v» вершина : «vt» UVCoord)
/////
///////////////////////////////// READ VERTEX INFO \\\\\\\\\\\\\\\\*
void CLoadObj::ReadVertexInfo()
{
CVector3 vNewVertex = {0};
CVector2 vNewTexCoord = {0};
char strLine[255] = {0};
char ch = 0;
// Читаем второй сисвол строки, чтобы увидеть, что содержит строка: вершины/нормали/UV
ch = fgetc(m_FilePointer);
if(ch == ‘ ‘) // Если только пробел — далее идёт только иняормация о вершинах («v»)
{
// Читаем вершину. Формат: «v x y z»
fscanf(m_FilePointer, «%f %f %f», &vNewVertex.x;, &vNewVertex.y;, &vNewVertex.z;);
// Читаем остальную линию, чтобы перейти к следующей
fgets(strLine, 100, m_FilePointer);
// Добавляем новую вершину в список
m_pVertices.push_back(vNewVertex);
}
else if(ch == ‘t’) // Если второй символ — ‘t’, строка содержит UV координаты («vt»)
{
// Читаем текстурные координаты. Формат: «vt u v»
fscanf(m_FilePointer, «%f %f», &vNewTexCoord.x;, &vNewTexCoord.y;);
// Читаем оставшуюся линию, чтобы перейти к следующей
fgets(strLine, 100, m_FilePointer);
// Вносим новые текстурные координаты в список
m_pTextureCoords.push_back(vNewTexCoord);
// Активируем флаг, сообщающий, что обьект имеет текстурные координаты.
// Теперь мы знаем, что строки полигонов будут содержать индексы не только
// вершин, но и текст. координат («f 1/1 2/2 3/3»)
m_bObjectHasUV = true;
}
else // Иначе это, видимо, нормаль, и нам она не нужна («vn»)
{
// Мы рассчитываем собственные нормали, так что пропустим строку
fgets(strLine, 100, m_FilePointer);
}
}
///////////////////////////////// READ FACE INFO \\\\\\\\\\\\\\\\*
/////
///// Читает информацию полигона («f»)
/////
///////////////////////////////// READ FACE INFO \\\\\\\\\\\\\\\\*
void CLoadObj::ReadFaceInfo()
{
tFace newFace = {0};
char strLine[255] = {0};
// Функция читает информацию о полигонах обьекта.
// Эта информация — 3д точки, составляющие полигон, и UV координаты,
// если на обьект наложена текстура.
// Если обьект имеет текстурные координаты, формат строки будет
// такой: «f v1/uv1 v2/uv2 v3/uv3»
// Иначе такой: «f v1 v2 v3»
// Внимание! Всегда убеждайтесь, что вычитаете 1 из индексов, так как
// массивы в c++ начинаются с 0, а индексы в .obj начинаются с 1.
// Проверяем, имеет ли обьект текстурные координаты
if(m_bObjectHasUV )
{
// Читаем индексы вершин и текстурных координат.
fscanf(m_FilePointer, «%d/%d %d/%d %d/%d», &newFace.vertIndex;[0], &newFace.coordIndex;[0],
&newFace.vertIndex;[1], &newFace.coordIndex;[1],
&newFace.vertIndex;[2], &newFace.coordIndex;[2]);
}
else // если обьект НЕ содержит текстурных координат
{
// Читаем только индексы вершин
fscanf(m_FilePointer, «%d %d %d», &newFace.vertIndex;[0],
&newFace.vertIndex;[1],
&newFace.vertIndex;[2]);
}
// Читаем линию до конца, чтобы перейти к следующей
fgets(strLine, 100, m_FilePointer);
// Добавляем новый полигон в список
m_pFaces.push_back(newFace);
// Устанавливаем этот флаг в TRUE, чтобы знать, что только что читали полигон. Если
// после этого читается вершина — значит, мы перешли к следующему обьекту и нужно
// сохранить этот.
m_bJustReadAFace = true;
}