Создание игр » Featured, Toolset » Загрузка модели из 3d MAX
Загрузка модели из 3d MAX
Загрузка модели делается лишь немного сложнее, чем делается экспорт с помощью maxscript. Хочу только, до того, как мы начнём разговор непосредственно о загрузке моделей, уточнить кое-какие моменты. Ну а теперь давайте продолжим разговор про загрузку моделей из файла, в который мы их экспортировали нашим экспортером моделей, написанным нами в предыдущем уроке. В нашем солюшене (Visual Studio Solution file) я сделал новый проект, дему 3д-игры – с этого момента в нашем солюшене будет три проекта – графический движок, 2д-игра и 3д-игра. Они все будут в одном солюшене, что бы Вы не путались в отдельных туторах и Вам не приходилось собирать каждую игру из кучи отдельных проектов.
Этапы загрузки модели
Многие думают, что всё, что надо для загрузки модели, это просто открыть файл с загружаемой моделью и прочитать из него данные. Однако, чаще всего загрузка модели состоит из нескольких этапов, количество которых может варьироваться в зависимости от формата хранения модели и от потребностей движка. Как правило, это следующие этапы:
- Загрузка данных модели из файла – то самое открытие файла и чтение данных модели из него;
- После загрузки модели иногда надо провести оптимизацию загруженной модели – на этом этапе модель оптимизируется под конкретные задачи, например, для ускорения рендеринга иногда нужно перегруппировать вертексы и грани модели под кэши выидео-карты, что бы модель рендерилась быстрее. Иногда в файле хранятся уже оптимизированные модели, иногда нет. Потому этот этап нужен не всегда;
- Загрузка модели в видео-карту делается для того, что бы хранить модель непосредственно в видео-карте, так отрисовка происходит значительно быстрее, кроме того, если модель хранится в видео-карте, появляется возможность использовать так называемый инстансинг, для ещё большего ускорения отрисовки множетсва одинаковых моделей;
- Последний этап – освобождение более ненужных ресурсов загруженной модели. При загрузке модели чаще всего создаются специальные временные буферы, для хранения загруженных данных. Потом эти данные оптимизируются, загружаются в видео-карту и после этого исходные данные становятся не нужны – мы должны их удалить, что бы не расходовать попусту ресурсы компьютера
Класс модели
Конечно же, для работы с моделью будет проще всего создать отдельный класс – работать с ООП всегда проще, чем работать на уровне обычных функций и глобальных данных. Исходя их списка этапов загрузки модели, мы уже вполне можем определить, какие данные и функции будут нам нужны в этом классе. Вот объявление класса модели:
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 и Вы поймёте. Если объяснять кратко, то работает это так:
- Открываем поток ввода из файла
- Создаём два итератора: начало потока и конец (итератор созданный по умолчанию всегда тот же самый, что указывает на конец любой последовательности и он равен нулю)
- Вызываем конструктор string, передавая ему итератор начала и конца
- А такой конкструктор стринга сам читает данные по переанным ему итераторам
- И, как следствие, мы получаем стринг, в котором находится всё содержимое файла
- При этом файл открыт как бинарный, потому файл прочитается в полностью неизменном виде
- А поскольку стринг (именно 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) – мы отдельно записали данные о позициях вертексов, отдельно данные о текстурных координатах, отдельно данные о гранях… В таком виде данные нельзя использовать, потому они и называются “сырые”. Теперь их надо правильно “приготовить”, что бы они стали “съедобными” для употребления видео-картой )))
Для этого нам необходимо собирать эти разрозненные части информации из разных массивов данных (позиции, текстурные координаты, нормали и т.д.) и собирать их в единый массив вертексов и массив индексов.
Можно обойтись и без массива индексов, но у индексов есть два существенных плюсов:
- Видеокарта почти всегда обрабатывает индексированные данные быстрее (об этом напишу в отдельной статье)
- Индексирование часто может уменьшить количество вертексов в модели. Например, если у нас есть два треугольника ( 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д-модели |
Раздел: Featured, Toolset · Теги: DirectX, Оптимизация, Создание движка
Ура, продолжаются уроки )))) СПАСИБО
Пожалуйста
+1000
Спасибо. Когда писал свой движок, мой способ больше напоминал геморрой.
Antony, Scripter, спасибо за отзывы
string GetFileAsString(const string& strFileName)
{
string content; // опечатка?
Just guest, верно, забыл стереть, спасибо!
спасибо, очень помогло.
На мой взгляд, очень важные моменты затрагиваются в этой теме.
Особенно понравились идеи с “кэшированными” индексами и потоками файлов. Я пока новичок в этом деле и предстоит еще многому научиться. Но благодаря подобным блогам и людям, готовым поделиться своими знаниями, многие начинающие гэйм-программисты вроде меня натворят гораздо меньше ошибок.
В общем, еще раз спасибо за уроки.