Теперь пришло время погрузиться в дебри Assimp и приступить к созданию кода, отвечающего за загрузку модели. Целью данного урока является создание класса, представляющего всю модель целиком, то есть модель, содержащую несколько мешей с, возможно, несколькими текстурами. Дом с деревянным балконом, башней и бассейном — всё это может быть загружено в виде единой модели. Мы загрузим модель через Assimp и переведем её в набор Mesh-объектов, которые создали на предыдущем уроке.
Структура класса Model
Без лишних слов я представляю вам структуру класса Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Model { public: Model(char *path) { loadModel(path); } void Draw(Shader &shader); private: // Данные модели vector<Mesh> meshes; string directory; void loadModel(string path); void processNode(aiNode *node, const aiScene *scene); Mesh processMesh(aiMesh *mesh, const aiScene *scene); vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName); }; |
Закрытый (private) раздел класса Model
содержит вектор, состоящий из объектов типа Mesh
. Далее при создании объекта класса конструктор запрашивает расположение файла модели. Затем он сразу же загружает файл с помощью функции loadModel(), которая вызывается в нем же. Все функции закрытого раздела класса предназначены для обработки процедуры импорта моделей Assimp-ом, они будут рассмотрены чуть позже. Чуть не забыл, также необходимо сохранить путь к папке с файлом модели, чтобы позже воспользоваться им при загрузке текстур.
Функция Draw() по сути является лишь оберткой для цикла for, с помощью которого мы проходимся по всем мешам, вызывая у каждого его собственную функцию Draw():
1 2 3 4 5 |
void Draw(Shader &shader) { for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader); } |
Импортирование 3D-модели в OpenGL
Чтобы импортировать модель и перевести её в нашу собственную структуру данных, нам, прежде всего, необходимо подключить соответствующие заголовочные файлы библиотеки Assimp:
1 2 3 |
#include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> |
Первая функция, которую мы используем — это loadModel(), она вызывается непосредственно из конструктора. Внутри loadModel() мы задействуем библиотеку Assimp для загрузки модели в структуру данных Assimp, называемую объектом сцены. Как только мы получим объект сцены, то будем иметь доступ ко всем необходимым данным загруженной модели.
Самое замечательное в Assimp то, что библиотека может аккуратно создать слой абстракции, отделяя нас от кучи технических деталей загрузки данных из различных форматов файлов, с помощью одной строки кода:
1 2 |
Assimp::Importer importer; const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); |
Сначала мы объявляем объект importer
типа Importer
из пространства имен Assimp, а затем вызываем из созданного объекта функцию ReadFile(). В качестве аргументов функция ожидает получить путь к файлу модели и несколько параметров постобработки. Assimp позволяет нам указать несколько параметров, которые заставляют библиотеку выполнять дополнительные вычисления/операции с импортированными данными. Задавая параметр aiProcess_Triangulate
, мы сообщаем Assimp, что если модель не состоит полностью из треугольников, то необходимо сначала преобразовать все примитивные формы модели в треугольники. Параметр aiProcess_FlipUVs
переворачивает во время обработки координаты текстуры на оси y, где это необходимо. Но это не все опции, есть еще несколько:
aiProcess_GenNormals
— создает нормальные векторы для каждой вершины, если модель не содержит нормальных векторов.
aiProcess_SplitLargeMeshes
— разбивает большие меши на несколько мешей меньшего размера, что полезно, когда ваш рендеринг уже содержит максимально разрешенное количество вершин или может обрабатывать только меши небольших размеров.
aiProcess_OptimizeMeshes
— делает обратное, пытаясь объединить несколько мешей в один большой меш, тем самым улучшая оптимизацию программы за счет уменьшения количества вызовов функции отрисовки объекта.
Assimp предоставляет нам большой набор опций постобработки, с полным списком которых вы можете ознакомиться здесь. Загрузка модели через Assimp (как вы можете видеть) удивительно проста. Самая сложная часть работы заключается в том, чтобы, используя возвращаемый объект сцены, преобразовать загруженные данных в массив объектов типа Mesh
.
Ниже представлен полный исходный код функции loadModel():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void loadModel(string path) { Assimp::Importer import; const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl; return; } directory = path.substr(0, path.find_last_of('/')); processNode(scene->mRootNode, scene); } |
После загрузки модели мы должны удостовериться, что переменные сцены и корневого узла сцены не являются нулевыми, а также с помощью проверки одного из флагов сцены убеждаемся, что возвращаемые данные являются полными. Если какое-либо из этих условий не выполняется, то сообщаем об ошибке, возвращая её описание при помощи функции GetErrorString(). Вдобавок ко всему извлекаем путь к папке с файлом модели.
Если процесс загрузки модели прошел успешно, то начинаем обрабатывать все узлы сцены. Передаем первый узел (корневой) рекурсивной функции processNode(). Поскольку каждый узел (возможно) содержит набор дочерних элементов, то необходимо сначала обработать выбранный узел, а затем продолжить обработку всех его дочерних элементов и так далее. В нашем случае условие выхода из рекурсии выполнится тогда, когда все узлы будут обработаны.
Как вы помните из строения структуры данных в Assimp, каждый узел содержит набор индексов мешей, указывающих на определенный меш в объекте сцены. Таким образом, сначала нам необходимо получить меш-индексы, затем получить каждый отдельный меш, обработать его, и под конец снова выполнить все эти действия для каждого из дочерних узлов. Содержимое функции processNode() показано ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void processNode(aiNode *node, const aiScene *scene) { // Обрабатываем все меши (если они есть) у выбранного узла for(unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene)); } // И проделываем то же самое для всех дочерних узлов for(unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); } } |
Сначала мы проверяем каждый меш-индекс выбранного узла и извлекаем соответствующий меш, индексируя массив mMeshes
сцены. Затем полученный меш передается в функцию processMesh(), которая возвращает объект типа Mesh
, который теперь мы можем сохранить в переменной meshes
типа список/вектор.
После того, как все меши будут обработаны, мы начнем перебирать дочерние элементы выбранного узла и вызывать одну и ту же функцию processNode() для каждого дочернего элемента. Как только мы переберем все дочерние элементы узла, рекурсия прекратится.
Примечание: Внимательный читатель, возможно, заметил, что мы могли бы и не обрабатывать узлы, а просто напрямую пройтись по всем мешам сцены, не делая все эти сложные вещи с индексами. Причина, по которой мы всё это делаем, заключается в том, что идея использования узлов задает между мешами отношение типа «родитель-ребенок». Рекурсивно перебирая эти цепочки, мы можем определить те меши, которые являются родительскими элементами по отношению к другим мешам. Пример использования такой системы — это когда вы хотите преобразовать меш автомобиля и убедиться, что все его дочерние элементы (например, меш двигателя, меш рулевого колеса и меш шин) также преобразуются; такая система легко создается с использованием отношений типа «родитель-ребенок».
Однако сейчас мы не используем такую систему, но рекомендуется придерживаться данного подхода всякий раз, когда вы хотите получить дополнительный контроль над своими меш-данными.
Следующий шаг — это обработка Assimp-данных и перевод их в Mesh-класс, описанный на предыдущем уроке.
Из Assimp в Mesh
Перевод объекта aiMesh
в наш собственный меш-объект не слишком сложен. Всё, что нам нужно сделать — это получить доступ к каждому из соответствующих свойств меша и сохранить их в нашем собственном объекте. Общая структура функции processMesh() следующая:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Mesh processMesh(aiMesh *mesh, const aiScene *scene) { vector<Vertex> vertices; vector<unsigned int> indices; vector<Texture> textures; for(unsigned int i = 0; i < mesh->mNumVertices; i++) { Vertex vertex; // Обрабатываем координаты вершин, нормали и текстурные координаты [...] vertices.push_back(vertex); } // Обрабатываем индексы [...] // Обрабатываем материал if(mesh->mMaterialIndex >= 0) { [...] } return Mesh(vertices, indices, textures); } |
Обработка меша — это процесс, состоящий из 3 частей:
извлечение всех вершинных данных;
извлечение индексов меша;
извлечение соответствующих данных о материале.
Обработанные данные хранятся в 3 векторах, из которых впоследствии создается объект типа Mesh
и возвращается вызывающему объекту функции.
Извлечение вершинных данных — дело довольно простое: мы определяем структуру Vertex
, которую добавляем в массив vertices
после каждой итерации цикла. Количество итераций цикла зависит от количества вершин, содержащихся в меше (эту информацию можно получить через mesh->mNumVertices
). В рамках итерации мы заполняем структуру всеми соответствующими данными. Для вершинных координат это делается следующим образом:
1 2 3 4 5 |
glm::vec3 vector; vector.x = mesh->mVertices[i].x; vector.y = mesh->mVertices[i].y; vector.z = mesh->mVertices[i].z; vertex.Position = vector; |
Обратите внимание, что мы определяем временную переменную типа vec3 для передачи Assimp-данных. Это необходимо, так как библиотека Assimp поддерживает свои собственные типы данных для вектора, матрицы, строки и т.п., и они не очень хорошо преобразуются в типы данных GLM.
Assimp именует свой массив координат вершин как mVertices
. Согласитесь, что это не самое интуитивно понятное название.
Следующий код не должен вызывать у вас удивление:
1 2 3 4 |
vector.x = mesh->mNormals[i].x; vector.y = mesh->mNormals[i].y; vector.z = mesh->mNormals[i].z; vertex.Normal = vector; |
С текстурными координатами происходит примерно то же самое, но при этом стоит заметить, что Assimp позволяет модели иметь до 8 различных координат текстуры на вершину. Мы не собираемся использовать все 8, нас волнует только первый набор текстурных координат. Также необходимо проверить, действительно ли меш содержит текстурные координаты (их может и не быть вовсе):
1 2 3 4 5 6 7 8 9 |
if(mesh->mTextureCoords[0]) // действительно ли меш содержит текстурные координаты? { glm::vec2 vec; vec.x = mesh->mTextureCoords[0][i].x; vec.y = mesh->mTextureCoords[0][i].y; vertex.TexCoords = vec; } else vertex.TexCoords = glm::vec2(0.0f, 0.0f); |
Теперь структура vertex
полностью заполнена необходимыми атрибутами вершин, и на заключительном этапе итерации мы можем добавить её в конец вектора vertices
. Данный процесс будет повторяться для каждой вершины меша.
Индексы
Интерфейс Assimp определяет каждый меш как объект, содержащий массив граней, при этом каждая грань представляет собой отдельный примитив, который в нашем случае (из-за опции aiProcess_Triangulate
) всегда является треугольником. Грань содержит индексы вершин примитива в том порядке, в каком мы должны их отрисовывать. Так что, если мы переберем все грани и сохраним индексы в векторе indices
, то у нас всё будет готово:
1 2 3 4 5 6 |
for(unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for(unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } |
Теперь, после завершения внешнего цикла for, мы будем иметь полный набор вершин и индексов, необходимый для отрисовки меша с помощью функции glDrawElements(). Однако это еще не всё; чтобы добавить к мешу немного деталей, необходимо обработать данные о материале меша.
Материал
Подобно узлам, меш содержит только индекс объекта материала. Чтобы получить сам материал меша, нам нужно проиндексировать массив mMaterials
сцены. Индекс материала меша задается в его свойстве mMaterialIndex
, которое мы также можем запросить, чтобы проверить, содержит ли меш материал или нет:
1 2 3 4 5 6 7 8 9 10 |
if(mesh->mMaterialIndex >= 0) { aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex]; vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); } |
Сначала мы извлекаем объект aiMaterial
из массива mMaterials
сцены. Затем загружаем диффузные и/или зеркальные текстуры меша. Объект материала хранит внутри себя массив местоположений текстуры для каждого типа текстуры. Все различные типы текстур имеют префикс aiTextureType_
. Для извлечения, загрузки и инициализации текстуры из материала мы используем вспомогательную функцию loadMaterialTextures(). Она возвращает вектор, содержащий элементы в виде структур Textures
, которые будут сохранены в конце вектора модели textures
.
Функция loadMaterialTextures() выполняет итерацию по всем локациям текстур заданного типа, извлекает расположение файла текстуры, а затем загружает и генерирует текстуру, и сохраняет информацию в структуре Vertex
. Это выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector<Texture> textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); Texture texture; texture.id = TextureFromFile(str.C_Str(), directory); texture.type = typeName; texture.path = str; textures.push_back(texture); } return textures; } |
Сначала мы проверяем количество текстур, хранящихся в материале, с помощью функции GetTextureCount(), которая ожидает получить в качестве параметра один из переданных нами типов текстур. Далее извлекаем каждое расположение файла текстуры с помощью функции GetTexture(), которая сохраняет результат в переменной типа aiString
. Затем мы используем другую вспомогательную функцию TextureFromFile(), которая загружает текстуру (с помощью заголовочного файла stb_image.h) и возвращает её идентификатор.
Примечание: Обратите внимание, что мы исходим из предположения, что пути к ресурсам текстур в файлах модели являются локальными относительно используемого объекта модели, например, они находятся в том же каталоге, что и сама модель. Затем мы можем просто объединить строку, содержащую местоположение текстуры, со строкой каталога, полученной ранее (в функции loadModel()), чтобы воссоздать полный путь к текстуре (именно поэтому функция GetTexture() также нуждается в данной строке).
Некоторые модели, найденные в интернете, используют абсолютные пути для своих текстур, которые не на всех компьютерах будут работать. В таком случае вам потребуется вручную отредактировать файл, чтобы задать использование локальных путей для текстур (если это возможно).
И это всё, что нужно сделать для реализации импорта модели с помощью библиотеки Assimp.
Оптимизация
Но это еще не конец, так как есть довольно большая (но может не совсем необходимая) оптимизация, которую стоило бы провести. Большинство сцен повторно используют несколько своих текстур. Представьте себе еще раз дом, который имеет текстуру гранита для своих стен. Данная текстура также может быть применена и к полу, и к потолку, и к лестнице, и, возможно, даже к столу, и к небольшому колодцу поблизости. Загрузка текстур — это очень затратная операция, и в нашей текущей реализации для каждого меша загружается и генерируется новая текстура, хотя точно такая же текстура уже могла быть загружена ранее. В результате получаем узкое место в реализации загрузки нашей модели.
Поэтому необходимо добавить одну небольшую поправку к коду модели, сохранив все загруженные текстуры глобально. В том месте, где мы хотим загрузить текстуру, сначала нужно проверить, не была ли она уже загружена ранее. Если это так, то мы берем эту текстуру и пропускаем всю процедуру загрузки, экономя вычислительные ресурсы компьютера. Чтобы иметь возможность сравнивать текстуры нам нужно также сохранить их путь:
1 2 3 4 5 |
struct Texture { unsigned int id; string type; string path; // мы сохраняем путь к текстуре, чтобы сравнивать с другими текстурами }; |
Затем нам необходимо сохранить все загруженные текстуры в векторе, объявленном в верхней части файла класса модели в качестве private-переменной:
1 |
vector<Texture> textures_loaded; |
В функции loadMaterialTextures() мы сравниваем путь загружаемой текстуры со всеми текстурами из вектора textures_loaded
. Если есть совпадение, то мы пропускаем часть кода загрузки/генерации текстуры и просто используем найденную текстурную структуру в качестве текстуры меша. Обновленная функция показана ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector<Texture> textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); bool skip = false; for(unsigned int j = 0; j < textures_loaded.size(); j++) { if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) { textures.push_back(textures_loaded[j]); skip = true; break; } } if(!skip) { // Если текстура не была загружена ранее, то загружаем её Texture texture; texture.id = TextureFromFile(str.C_Str(), directory); texture.type = typeName; texture.path = str.C_Str(); textures.push_back(texture); textures_loaded.push_back(texture); // добавляем её к списку загруженных текстур } } return textures; } |
Примечание: Некоторые версии Assimp при использовании режима отладки Debug вашей IDE, как правило, загружают модели довольно медленно, поэтому обязательно протестируйте работу Assimp с Release-версиями приложения в вашей IDE.
Полный исходный код класса Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
#ifndef MODEL_H #define MODEL_H #include <glad/glad.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include "stb_image.h" #include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> #include "mesh.h" #include "shader.h" #include <string> #include <fstream> #include <sstream> #include <iostream> #include <map> #include <vector> using namespace std; unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false); class Model { public: // Данные модели vector<Texture> textures_loaded; // (оптимизация) сохраняем все загруженные текстуры, чтобы убедиться, что они не загружены более одного раза vector<Mesh> meshes; string directory; bool gammaCorrection; // Конструктор, который в качестве аргумента использует путь к 3D-модели Model(string const &path, bool gamma = false) : gammaCorrection(gamma) { loadModel(path); } // Выполняем отрисовку модели и всех её мешей void Draw(Shader shader) { for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader); } private: // Загружаем модель с помощью Assimp и сохраняем полученные меши в векторе meshes void loadModel(string const &path) { // Чтение файла с помощью Assimp Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); // Проверка на ошибки if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // если НЕ 0 { cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl; return; } // Получение пути к файлу directory = path.substr(0, path.find_last_of('/')); // Рекурсивная обработка корневого узла Assimp processNode(scene->mRootNode, scene); } // Рекурсивная обработка узла. Обрабатываем каждый отдельный меш, расположенный в узле, и повторяем этот процесс для своих дочерних узлов (если таковые вообще имеются) void processNode(aiNode *node, const aiScene *scene) { // Обрабатываем каждый меш текущего узла for(unsigned int i = 0; i < node->mNumMeshes; i++) { // Узел содержит только индексы объектов в сцене. // Сцена же содержит все данные; узел - это лишь способ организации данных aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene)); } // После того, как мы обработали все меши (если таковые имелись), мы начинаем рекурсивно обрабатывать каждый из дочерних узлов for(unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); } } Mesh processMesh(aiMesh *mesh, const aiScene *scene) { // Данные для заполнения vector<Vertex> vertices; vector<unsigned int> indices; vector<Texture> textures; // Цикл по всем вершинам меша for(unsigned int i = 0; i < mesh->mNumVertices; i++) { Vertex vertex; glm::vec3 vector; // объявляем промежуточный вектор, т.к. Assimp использует свой собственный векторный класс, который не преобразуется напрямую в тип glm::vec3, поэтому сначала мы передаем данные в этот промежуточный вектор типа glm::vec3 // Координаты vector.x = mesh->mVertices[i].x; vector.y = mesh->mVertices[i].y; vector.z = mesh->mVertices[i].z; vertex.Position = vector; // Нормали vector.x = mesh->mNormals[i].x; vector.y = mesh->mNormals[i].y; vector.z = mesh->mNormals[i].z; vertex.Normal = vector; // Текстурные координаты if(mesh->mTextureCoords[0]) // если меш содержит текстурные координаты { glm::vec2 vec; // Вершина может содержать до 8 различных текстурных координат. Мы предполагаем, что не будем использовать модели, // в которых вершина может содержать несколько текстурных координат, поэтому всегда берем первый набор (0) vec.x = mesh->mTextureCoords[0][i].x; vec.y = mesh->mTextureCoords[0][i].y; vertex.TexCoords = vec; } else vertex.TexCoords = glm::vec2(0.0f, 0.0f); // Касательный вектор vector.x = mesh->mTangents[i].x; vector.y = mesh->mTangents[i].y; vector.z = mesh->mTangents[i].z; vertex.Tangent = vector; // Вектор бинормали vector.x = mesh->mBitangents[i].x; vector.y = mesh->mBitangents[i].y; vector.z = mesh->mBitangents[i].z; vertex.Bitangent = vector; vertices.push_back(vertex); } // Теперь проходимся по каждой грани меша (грань - это треугольник меша) и извлекаем соответствующие индексы вершин for(unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; // Получаем все индексы граней и сохраняем их в векторе indices for(unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } // Обрабатываем материалы aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; // Мы вводим соглашение об именах сэмплеров в шейдерах. Каждая диффузная текстура будет называться 'texture_diffuseN', // где N - это порядковый номер от 1 до MAX_SAMPLER_NUMBER. // То же самое относится и к другим текстурам: // диффузная - texture_diffuseN // отражения - texture_specularN // нормали - texture_normalN // высоты - texture_heightN // 1. Диффузные карты vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); // 2. Карты отражения vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); // 3. Карты нормалей std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal"); textures.insert(textures.end(), normalMaps.begin(), normalMaps.end()); // 4. Карты высот std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height"); textures.insert(textures.end(), heightMaps.begin(), heightMaps.end()); // Возвращаем Mesh-объект, созданный на основе полученных данных return Mesh(vertices, indices, textures); } // Проверяем все текстуры материалов заданного типа и загружам текстуры, если они еще не были загружены. // Необходимая информация возвращается в виде struct Texture vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector<Texture> textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); // Проверяем, не была ли текстура загружена ранее, и если - да, то пропускаем загрузку новой текстуры и переходим к следующей итерации bool skip = false; for(unsigned int j = 0; j < textures_loaded.size(); j++) { if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) { textures.push_back(textures_loaded[j]); skip = true; // текстура с тем же путем к файлу уже загружена, переходим к следующей (оптимизация) break; } } if(!skip) { // Если текстура еще не была загружена, то загружаем её Texture texture; texture.id = TextureFromFile(str.C_Str(), this->directory); texture.type = typeName; texture.path = str.C_Str(); textures.push_back(texture); textures_loaded.push_back(texture); // сохраняем её в массиве с уже загруженными текстурами, тем самым гарантируя, что у нас не появятся дубликаты текстур } } return textures; } }; unsigned int TextureFromFile(const char *path, const string &directory, bool gamma) { string filename = string(path); filename = directory + '/' + filename; unsigned int textureID; glGenTextures(1, &textureID); int width, height, nrComponents; unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0); if (data) { GLenum format; if (nrComponents == 1) format = GL_RED; else if (nrComponents == 3) format = GL_RGB; else if (nrComponents == 4) format = GL_RGBA; glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Texture failed to load at path: " << path << std::endl; stbi_image_free(data); } return textureID; } #endif |
Больше никаких контейнеров!
Ну что же, давайте придадим новый импульс нашей реализации, импортировав модель, созданную настоящими 3D-дизайнерами. Давайте загрузим удивительный гитарный рюкзак выживальщика от Berk Gedik. Я немного изменил материал и пути, чтобы они работали непосредственно с тем, как мы настроили загрузку модели. Модель экспортируется в виде файла .obj вместе с файлом .mtl, который ссылается на диффузные, зеркальные и нормальные карты модели (мы вернемся к ним позже). Вы можете скачать подкорректированную модель здесь. Обратите внимание, что есть несколько дополнительных типов текстур, которые мы пока не будем использовать, и что все текстуры и файлы моделей должны быть расположены в одном каталоге для загрузки текстур.
Примечание: Модифицированная версия рюкзака использует локальные относительные пути текстуры, и у нее переименованы текстуры альбедо и металла в диффузные и зеркальные, соответственно.
Теперь объявим объект типа Model
и передадим ему путь к файлу модели. Затем модель должна автоматически загрузиться, и (если не было ошибок) в цикле рендеринга нам нужно будет визуализировать объект, используя функцию Draw(), и это всё! Больше никаких выделений памяти под буферы, указателей атрибутов и команд рендеринга, только однострочный код загрузки модели. Если мы создадим простой набор шейдеров, в котором фрагментный шейдер выводит исключительно диффузную текстуру объекта, результат будет выглядеть примерно так:
GitHub / Урок №18. Загрузка моделей в OpenGL — Исходный код
Обратите внимание, что мы сообщаем stb_image.h перевернуть текстуры вертикально (если вы этого еще не сделали до загрузки модели). В противном случае текстуры будут выглядеть совершенно испорченными.
Мы также можем проявить более творческий подход и ввести точечные источники света в уравнение рендеринга, и вместе с зеркальными картами получить удивительные результаты:
Даже я должен признать, что это выглядит, возможно, немного более причудливо, чем контейнеры, которые мы использовали до сих пор. С помощью Assimp вы можете загрузить тонны различных моделей, найденных в Интернете. Существует довольно много сайтов, которые предлагают бесплатные 3D-модели для загрузки в разных форматах. Обратите внимание, что некоторые модели все еще не загружаются должным образом, имеют пути текстур, которые не будут работать, или просто экспортируются в формате, который даже библиотека Assimp не в состоянии прочитать.
Дополнительные ресурсы
How-To Texture Wavefront (.obj) Models for OpenGL — это отличное видео-руководство Мэтью Эрли о том, как настроить 3D-модели в Blender, чтобы они напрямую работали с текущим загрузчиком моделей (поскольку выбранная нами настройка текстуры не всегда работает «из коробки»).
Спасибо за урок.
Пришлось повозиться с освещением: избавиться от структуры Material (хотя можно было бы изменить названия текстур в классах Mesh и Model). Рюкзачёк сразу заиграл красками.
Загружается конечно довольно долго (около 12 секунд), пробовал release, но что-то не пошло.
Удалось найти робота в формате .obj (хотя файла .mtl не было). Сообразил взять mtl от рюкзака (открываетя обычным текстовым редактором) и чуток его поковырять. У робота был файл эмиссии (светился глаз) но почему-то формат jpg портил всю картину. Переписал в png тогда глаз (и некоторые детали) чётко засветились. Да… и пришлось его подгрузить вместо текстуры нормалей (просто устал искать формат файла mtl и какой идентификатор цеплять на эмиссию).
Долго морочил голову с картой нормалей. Хорошо что google вернул на сайт ravesli урок 34 (отложу на потом).
Здравствуйте, скажите, пожалуйста, будет ли в будущих уроках показан импорт файлов формата .fbx так как это сейчас очень популярный формат 3Д моделей, хотелось бы понимать как работать именно с ним.
Здравствуйте, у меня возникла проблема с касательными векторами модели:
Вызвано необработанное исключение: нарушение доступа для чтения. mesh->mTangents было 0x1110112
Дошел пока до шестого урока и заглянул сюда. Данный класс подходит только для obj и или для любого формата файлов?
Здравствуйте
Столкнулся с проблемой. При добавлении точечного источника света, отраженный свет по какой то причине отображается только в красном спектре. Когда экспериментировал с шейдером обнаружил, что красный оттенок исходит от карты отраженного света. На на этой карте вообще нет красного оттенка.
При этом фоновый и рассеянный свет работают без нареканий.
В чем может быть проблема?
Тяжело что-то подсказать, не имея перед собой исходный код вашего шейдера…
Я использую следующий фрагментный шейдер:
Дело в том, что текстура specular имеет только первый и единственный канал (вы можете проверить в любом редакторе, что на самом деле это GrayScale текстура).
Вы используете vec3(texture(material.diffuse, TexCoords)), поэтому у вас берется красный из текстуры [0.0 : 1.0], а зеленый и синий — это всегда 0, потому что их нет текстуре.
Как вариант можно написать так:
или сокращенно:
В этом случае первый канал будет задействован во всех трех компонентах вектора (что нам и надо, т.к. мы затем полученный vec3 спекуляра материала покомпонентно умножаем на vec3 спекуляра источника света и т.д.)
Здравствуйте, а как загрузить модель формата stl одним файлом?
Немного не понял ваш вопрос, в смысле "одним файлом"?
Ну помимо .obj файла нужен еще .mtl файл модели, а у формата .stl только один .stl нету .mtl. Как вместо .obj файла загрузить .stl?
А можно ли попросить небольшой пример того, как правильно к мешу применять источники света?
Как должен выглядеть фрагментный шейдер в данном случае и как правильно изменять параметры света
Потому что на данный момент я ловлю черный рюкзак с небольшими вкраплениями нужной текстуры
В вашем коде на этой строке у меня сегфолт:
Пересобрал assimp сам — не помогло. Платформа GNU/Linux
Об этом как раз я и писал в Уроке №16
https://ravesli.com/urok-16-biblioteka-importa-3d-modelej-assimp-v-opengl/#toc-4
"К сожалению, под данную платформу мне не удалось заставить стабильно работать собранную из исходников библиотеку Assimp. С ней постоянно возникали какие-то проблемы […]
Поэтому пока пользователям Linux приходится рассчитывать только на свои собственные силы. Увы…"
Возникла проблема с загрузкой текстур: при использовании функции TextureFromFile(); пишет "необъявленный идентификатор"
stb_image.h подключал в точности как в уроке 6, все другие функции работают
Не подскажете, в чем дело?
"Необъявленный идентификатор" чего? Он ругается, что не может найти функцию TextureFromFile()? Или какую-то переменную? Если функцию — то она определена в заголовочном файле model.h.
Проверьте как у вас подключаются заголовочные файлы в файле main.cpp. У меня они идут в следующем порядке:
А, да, извиняюсь. Проверил model.h , оказалось, проблема там
Спасибо большое!
Пожалуйста 🙂