Этот урок показывает способ загрузки моделей из файлов .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:

#ifndef _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 «main.h»
#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;
}