Уроки Коммент.

Создание игр » Featured, Toolset » Загрузка модели из 3d MAX

Загрузка модели из 3d MAX

Load model from 3d max Загрузка модели делается лишь немного сложнее, чем делается экспорт с помощью maxscript. Хочу только, до того, как мы начнём разговор непосредственно о загрузке моделей, уточнить кое-какие моменты. Ну а теперь давайте продолжим разговор про загрузку моделей из файла, в который мы их экспортировали нашим экспортером моделей, написанным нами в предыдущем уроке. В нашем солюшене (Visual Studio Solution file) я сделал новый проект, дему 3д-игры – с этого момента в нашем солюшене будет три проекта – графический движок, 2д-игра и 3д-игра. Они все будут в одном солюшене, что бы Вы не путались в отдельных туторах и Вам не приходилось собирать каждую игру из кучи отдельных проектов.

Этапы загрузки модели

Многие думают, что всё, что надо для загрузки модели, это просто открыть файл с загружаемой моделью и прочитать из него данные. Однако, чаще всего загрузка модели состоит из нескольких этапов, количество которых может варьироваться в зависимости от формата хранения модели и от потребностей движка. Как правило, это следующие этапы:

  1. Загрузка данных модели из файла – то самое открытие файла и чтение данных модели из него;
  2. После загрузки модели иногда надо провести оптимизацию загруженной модели – на этом этапе модель оптимизируется под конкретные задачи, например, для ускорения рендеринга иногда нужно перегруппировать вертексы и грани модели под кэши выидео-карты, что бы модель рендерилась быстрее. Иногда в файле хранятся уже оптимизированные модели, иногда нет. Потому этот этап нужен не всегда;
  3. Загрузка модели в видео-карту делается для того, что бы хранить модель непосредственно в видео-карте, так отрисовка происходит значительно быстрее, кроме того, если модель хранится в видео-карте, появляется возможность использовать так называемый инстансинг, для ещё большего ускорения отрисовки множетсва одинаковых моделей;
  4. Последний этап – освобождение более ненужных ресурсов загруженной модели. При загрузке модели чаще всего создаются специальные временные буферы, для хранения загруженных данных. Потом эти данные оптимизируются, загружаются в видео-карту и после этого исходные данные становятся не нужны – мы должны их удалить, что бы не расходовать попусту ресурсы компьютера

Класс модели

Конечно же, для работы с моделью будет проще всего создать отдельный класс – работать с ООП всегда проще, чем работать на уровне обычных функций и глобальных данных. Исходя их списка этапов загрузки модели, мы уже вполне можем определить, какие данные и функции будут нам нужны в этом классе. Вот объявление класса модели:

class CMesh
{
	//! вертексы модели
	std::vector<VertPosNormalTc> m_vecVerts;
 
	//! фэйсы. по 3 индекса на каждый Face
	std::vector<WORD> m_vecFaces;
 
	//! Добавляет вертекс в массив и возвращает его индекс
	WORD AddVert(const VertPosNormalTc& vert);
	//! Добавляет вертекс и заносит его индекс в массив индексов
	void AddFaceVert(const VertPosNormalTc& vert);
 
public:
	CMesh(void);
	virtual ~CMesh(void);
	virtual bool Load(const std::string& strFileName);
};

Загрузка данных модели

Загрузка самих данных модели из файла, в общем-то, совершенно тривиальна. Единственное, на что мне хотелось бы обратить ваше внимание, это на то, что я немного изменил саму функцию загрузки содержимого файла (функция GetFileAsString), переписал её, так сказать, в STL-style и выглядит она теперь вот так:

string GetFileAsString(const string& strFileName)
{
	std::ifstream in(strFileName.c_str(), std::ios::binary);
	std::istreambuf_iterator<char> begin(in), end;
	return string( begin, end );
}

Вы сможете понять, что тут происходит? Почитайте документацию по использованным мною классам STL и Вы поймёте. Если объяснять кратко, то работает это так:

  1. Открываем поток ввода из файла
  2. Создаём два итератора: начало потока и конец (итератор созданный по умолчанию всегда тот же самый, что указывает на конец любой последовательности и он равен нулю)
  3. Вызываем конструктор string, передавая ему итератор начала и конца
  4. А такой конкструктор стринга сам читает данные по переанным ему итераторам
  5. И, как следствие, мы получаем стринг, в котором находится всё содержимое файла
  6. При этом файл открыт как бинарный, потому файл прочитается в полностью неизменном виде
  7. А поскольку стринг (именно std::string) может содержать в себе в том числе и нули и символы и перевода строки и т.д., мы можем использовать эту функцию, как функцию даже для чтения бинарных данных, при этом всё будет работать правильно

А вот исходник загрузки:

bool CMesh::Load( const std::string& strFileName )
{
	std::string contents = GetFileAsString(strFileName);
	if (contents.length()==0)
		return false;
 
	// поток ввода из "файла"
	std::stringstream ss(contents);
 
	// читаем количество вертексов, граней и текст. вертексов
	int iNumVerts, iNumFaces, iNumTVerts;
	ss>>iNumVerts>>iNumFaces>>iNumTVerts;
 
	vector<D3DXVECTOR3> vecPos;
	vector<D3DXVECTOR3> vecNormal;
	// резервируем место что бы при
	// добавлении не было лишних аллокаций
	vecPos.reserve(iNumVerts);
	vecNormal.reserve(iNumVerts);
	for (int i=0; i<iNumVerts; i++)
	{
		// читаем позиции
		ss>>vecPos[i].x>>vecPos[i].y>>vecPos[i].z;
		// читаем нормали
		ss>>vecNormal[i].x>>vecNormal[i].y>>vecNormal[i].z;
	}
	vector<D3DXVECTOR3> vecTc;
	vecTc.reserve(iNumTVerts);
	// читаем текстурные координаты
	// на самом деле нам нужны только две координаты (UV)
	// но раз в файле записано три - читаем три
	for (int i=0; i<iNumTVerts; i++)
		ss>>vecTc[i].x>>vecTc[i].y>>vecTc[i].z;
 
	vector<WORD> vecFace;
	vector<WORD> vecFaceTc;
	// читаем индексы
	// по три индекса на каждую грань
	vecFace.reserve(iNumFaces*3);
	vecFaceTc.reserve(iNumFaces*3);
	for (int i=0; i<iNumFaces*3; i+=3)
	{
		// читаем грани
		ss>>vecFace[i]>>vecFace[i+1]>>vecFace[i+2];
		ss>>vecFaceTc[i]>>vecFaceTc[i+1]>>vecFaceTc[i+2];
	}

Я постарался набить код комментариями по максимуму, потому Вам должно быть хорош понятно, что происходит. Единственным непонятным моментом тут может быть строка:

std::stringstream ss(contents);

Это я сделал из нашей сроки с содержимым файла поток ввода-вывода, что бы было удобнее читать данные. За подробностями прошу обращаться к справке по STL классу stringstream.

Сборка модели из загруженных данных

Как Вы, наверное, заметили, из 3d Studio Max мы выгрузили даные, так сказать, в “сыром” виде. Т.е. наш набор данных не представляет из себя набор вертексов, которые уже готовы к использованию в DirectX (или OpenGL) – мы отдельно записали данные о позициях вертексов, отдельно данные о текстурных координатах, отдельно данные о гранях… В таком виде данные нельзя использовать, потому они и называются “сырые”. Теперь их надо правильно “приготовить”, что бы они стали “съедобными” для употребления видео-картой )))

Для этого нам необходимо собирать эти разрозненные части информации из разных массивов данных (позиции, текстурные координаты, нормали и т.д.) и собирать их в единый массив вертексов и массив индексов.

Можно обойтись и без массива индексов, но у индексов есть два существенных плюсов:

  1. Видеокарта почти всегда обрабатывает индексированные данные быстрее (об этом напишу в отдельной статье)
  2. Индексирование часто может уменьшить количество вертексов в модели. Например, если у нас есть два треугольника ( 2 тр. * 3 верт = 6 верт ) и они имеют две общие вершины, индексы позволят нам хранить только 4 (крайних, угловых) вертекса вместо 6 – таким образом мы сокращаем потребления памяти на каждую модель

Оптимизация загруженной модели

Сокращение потребления памяти моделью является частью процесса оптимизации загружаемой модели – для некоторых моделей такая экономия может составлять до 60%, т.е. если модель без оптимизации занимала бы, скажем, 1 Мб вдеопамяти, то после оптимизации в тот же самый объём памяти может влезть 2, а иногда и 3 модели!

Я совместил этап загрузки данных модели и этап её оптимизации в пределах одной функции (CMesh::Load). Вот оставшийся код этой функции (начало см. выше):

	// собираем вертексы и добавляем их в модель
	for (int i=0; i<iNumFaces*3; i+=3)
	{
		VertPosNormalTc vert;
		// собираем вертекс
		vert.m_pos = vecPos[ vecFace[i] ];
		vert.m_normal = vecNormal[ vecFace[i] ];
		D3DXVECTOR3 tc = vecTc[ vecFaceTc[i] ];
		vert.m_tc = D3DXVECTOR2(tc.x, tc.y);
		// добавляем вертекс
		AddFaceVert(vert);
	}
	// я не сделал доп. проверки на ошибки
	// например, если в файле не хватает данных
	// попробуйте сделать это сами
	return true;
}

Функция AddFaceVert добавлет вертекс в нашу модель. Но она делает это немного хитро: она вызывает функцию простого добавления вертекса, получает от неё индекс добавленного вертекса и записывает его в массив индексов. А функция “простого добавления вертекса, каждый добавляемый вертекс проверяет на уникальность. Если же он не уникален – она его не добавляет, а просто возвращает индекс уже существующего вертекса. Таким образом мы удаляем дубликаты вертексов и, тем самым, экономим память:

WORD CMesh::AddVert( const VertPosNormalTc& vert )
{
	// ищем, нет ли такого же вертекса в массиве
	const size_t count = m_vecVerts.size();
	for (size_t i=0; i<count; i++)
		if (vert == m_vecVerts[i])
			return (WORD)i; // нашли, возвращаем индекс
 
	// не нашли - добавляем
	m_vecVerts.push_back(vert);
	return (WORD)m_vecVerts.size();
}
 
void CMesh::AddFaceVert( const VertPosNormalTc& vert )
{
	// добавляем вертекс и получаем его индекс
	const WORD idx = AddVert(vert);
	// добавляем индекс в массив граней
	m_vecFaces.push_back(idx);
}

Получается удобно, просто и эффективно. Единственное уточнение, которое надо сделать – я исхожу из того, что эта функция не будет использоваться для загрузки гигантских моделей, где количество вертексов перевалит за 64к вертексов – такие модели, как правило, не нужны, и если они у вас есть – значит, вашим моделлеры что-то делают не так )))

При выходе из этой функции все std::vector, используемые нами для временного хранения данных, уничтожаются, т.к. они являются локальными переменными этой функции и освобождают занимаемую ими память.

Хочу так же заранее предупредить, что это не окончательный код загрузчика – в следующих уроках мы многое поменяем.

Это всё, о чём я хотел рассказать Вам в этом уроке про загрузку моделей, экспортированных ранее из 3d Studio MAX. Надеюсь, я не зря потратил столько часов на “допиливание” этого урока и он получился достаточно понятным.

P/S Я так же добавил прям в нашу игру код для отрисовки загруженной модели в центре экрана ) Потом уберём его, сейчас он просто даст возможность видеть, что поучилось. Код:

// код отрисовки 3д-модели в центре экрана
const std::vector<VertPosNormalTc>& verts = mesh.GetVerts();
const std::vector<WORD>& idx = mesh.GetIdx();
 
static float a=0;
a+=.01f;
D3DXMatrixRotationY(&mat, a);
m_pVertexShader->SetMatrix("mWorld", &mat);
 
gr.SetTexture(0, pTexDiffuse);
gr.SetFVF(D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX1);
gr.DIPUP(D3DPT_TRIANGLELIST, 0, verts.size(), idx.size()/3, &idx[0],
	D3DFMT_INDEX16, &verts[0], sizeof(VertPosNormalTc));
// закончили отрисовку 3д-модели

Готовая демка
Исходники

»crosslinked«

Ещё по этой теме:




Раздел: Featured, Toolset · Теги: DirectX, Оптимизация, Создание движка

9 комментариев на "Загрузка модели из 3d MAX"
  1. L-ee-X пишет:

    Ура, продолжаются уроки )))) СПАСИБО :)

  2. Scripter пишет:

    Спасибо. Когда писал свой движок, мой способ больше напоминал геморрой.

  3. Вячеслав пишет:

    Antony, Scripter, спасибо за отзывы :-)

  4. Just guest пишет:

    string GetFileAsString(const string& strFileName)
    {
    string content; // опечатка?

    1. Вячеслав пишет:

      Just guest, верно, забыл стереть, спасибо!

  5. ssovec пишет:

    спасибо, очень помогло.

  6. Shadow пишет:

    На мой взгляд, очень важные моменты затрагиваются в этой теме.

    Особенно понравились идеи с “кэшированными” индексами и потоками файлов. Я пока новичок в этом деле и предстоит еще многому научиться. Но благодаря подобным блогам и людям, готовым поделиться своими знаниями, многие начинающие гэйм-программисты вроде меня натворят гораздо меньше ошибок.

    В общем, еще раз спасибо за уроки. :)

Оставить комментарий

*

Вы можете использовать это HTMLтеги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>