C3 PREN 34 2ModelLoading
C3 PREN 34 2ModelLoading
PROPIEDADES Y RENDERING
3.1 Color, Luz, sombreado, materiales y textura
3.2 Iluminación y sombreado
3.3 Fuentes de luz
3.4 Rendering
Model Loading
Models that were carefully designed by 3D artists in tools like Blender, 3DS Max or Maya. What we want
instead, is to import these models into the application.
The tools then automatically generate all the vertex coordinates, vertex normals, and texture coordinates while
exporting them to a model file format we can use. We, as graphics programmers, do have to care about these
technical details though.
https://www.blender.org/download/demo-files/
Model Loading – A model loading library
There are many different file formats where a common general structure between them usually does not exist. So if we want to
import a model from these file formats, we'd have to write an importer ourselves for each of the file formats we want to import.
Procedure: First load an object into a Scene object, recursively retrieve the corresponding
Mesh objects from each of the nodes (we recursively search each node's children), and
process each Mesh object to retrieve the vertex data, indices, and its material
properties. The result is then a collection of mesh data that we want to contain in a single
Model object.
Model Loading – Building Assimp
• Download the source code from:
https://github.com/assimp/assimp/releases
• Compile assimp:
Cmake→load folder→configure→generate→OpenProject
ADDITIONAL RESOURCES:
• https://assimp-docs.readthedocs.io/en/latest/about/introduction.html#using-the-pre-built-libraries-with-visual-studio
• https://youtu.be/W_Ey_YPUjMk
• https://learnopengl.com/Model-Loading/Assimp
Model Loading – Mesh
With Assimp we can load many different models into the application, but once loaded they're all stored in Assimp's
data structures. What we eventually want is to transform that data to a format that OpenGL understands so that
we can render the objects.
A mesh should at least need a set of vertices, where each vertex contains a
position vector, a normal vector, and a texture coordinate vector. A mesh
should also contain indices for indexed drawing, and material data in the
form of textures (diffuse/specular maps).
Now that we set the minimal requirements for a mesh class we can
define a vertex in OpenGL: Next to a Vertex struct we also want to organize
the texture data in a Texture struct:
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal; struct Texture {
glm::vec2 TexCoords; unsigned int id;
}; string type;
We store each of the required vertex attributes in a struct called };
Vertex.
Model Loading – Mesh class Mesh {
Knowing the actual representation of a vertex and a public:
texture we can start defining the structure of the mesh // mesh data
class: vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
void setupMesh()
{
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
&indices[0], GL_STATIC_DRAW);
// vertex positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
Model Loading – Draw (Rendering)
Thanks to the constructor we now have large lists of mesh data that we can use for rendering. We do need to setup
the appropriate buffers and specify the vertex shader layout via vertex attribute pointers.
void Draw(Shader &shader)
We don't know from the start how many (if any) textures the {
mesh has and what type they may have. unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
for(unsigned int i = 0; i < textures.size(); i++)
Each diffuse texture is named texture_diffuseN, and each specular {
texture should be named texture_specularN where N is any glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit
before binding
number ranging from 1 to the maximum number of texture // retrieve texture number (the N in diffuse_textureN)
samplers allowed. string number;
string name = textures[i].type;
uniform sampler2D texture_diffuse1; if(name == "texture_diffuse")
uniform sampler2D texture_diffuse2; number = std::to_string(diffuseNr++);
uniform sampler2D texture_diffuse3; else if(name == "texture_specular")
uniform sampler2D texture_specular1; number = std::to_string(specularNr++);
uniform sampler2D texture_specular2; shader.setFloat(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
By this convention we can process any amount of textures on a }
glActiveTexture(GL_TEXTURE0);
single mesh and the shader developer is free to use as many of // draw mesh
those as he wants by defining the proper samplers. glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
The complete mesh class file: mesh.h
Model Loading – Model
The next step is to create another class that represents a model in its entirety, that is, a model that contains
multiple meshes, possibly with multiple textures. We'll load the model via Assimp and translate it to multiple Mesh
objects we've created.
class Model
The class structure of the Model class: {
public:
• The Model class contains a vector of Mesh objects and requires us to Model(char *path)
{
give it a file location in its constructor.
loadModel(path);
• It then loads the file right away via the loadModel function that is }
called in the constructor. void Draw(Shader &shader);
• The private functions are all designed to process a part of Assimp's private:
// model data
import routine and we'll cover them shortly. vector<Mesh> meshes;
• We also store the directory of the file path that we'll later need when string directory;
loading textures. void loadModel(string path);
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
The Draw function is nothing special and basically loops over each vector<Texture> loadMaterialTextures(aiMaterial *mat,
of the meshes to call their respective Draw function: aiTextureType type, string typeName);
};
void Draw(Shader &shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
Importing a 3D model into OpenGL
The first function we're calling is loadModel, that's directly called from the constructor. #include <assimp/Importer.hpp>
Within loadModel, we use Assimp to load the model into a data structure of Assimp #include <assimp/scene.h>
called a scene object. Once we have the scene object, we can access all the data we #include <assimp/postprocess.h>
need from the loaded model.
Assimp::Importer importer;
The great thing about Assimp is that it neatly abstracts from all the technical const aiScene *scene = importer.ReadFile(path,
details of loading all the different file formats and does all this with a single aiProcess_Triangulate | aiProcess_FlipUVs);
one-liner:
• aiProcess_GenNormals: creates normal vectors for each vertex if the model doesn't contain normal vectors.
• aiProcess_SplitLargeMeshes: splits large meshes into smaller sub-meshes which is useful if your rendering has a maximum
number of vertices allowed and can only process smaller meshes.
• aiProcess_OptimizeMeshes: does the reverse by trying to join several meshes into one larger mesh, reducing drawing calls for
optimization. http://assimp.sourceforge.net/lib_html/postprocess_8h.html
The next step is in using the returned scene object to translate the
loaded data to an array of Mesh objects. The complete loadModel
function is listed here:
void loadModel(string path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
• We first check each of the node's mesh indices and retrieve the corresponding mesh by indexing the scene's mMeshes array.
The returned mesh is then passed to the processMesh function that returns a Mesh object that we can store in the meshes
list/vector.
• Once all the meshes have been processed, we iterate through all of the node's children and call the same processNode
function for each its children. Once a node no longer has any children, the recursion stops.
• The next step is to process Assimp's data into the Mesh class
Assimp to Mesh
The next step is to access each of the mesh's relevant properties and store them in our own object. The general structure of the
processMesh function then becomes:
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;
[...] // process vertex positions, normals and texture coordinates
vertices.push_back(vertex);
}
[...] // process indices
if(mesh->mMaterialIndex >= 0) // process material
{
[...]
}
return Mesh(vertices, indices, textures);
}
Assimp to Mesh - ProcessMesh
Vertex Data→ We define a Vertex Normals→ The procedure for
struct that we add to the vertices normals should come as no
array after each loop iteration. surprise now:
glm::vec3 vector;
vector.x = mesh->mNormals[i].x;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mNormals[i].y;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mNormals[i].z;
vector.z = mesh->mVertices[i].z;
vertex.Normal = vector;
vertex.Position = vector;
Texture coordinates are roughly the same, but Assimp allows a model to have up to 8 different texture coordinates per
vertex. We're not going to use 8, we only care about the first set of texture coordinates. We'll also want to check if the
mesh actually contains texture coordinates (which may not be always the case):
if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
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);
Assimp to Mesh – Indices / Materials
Each face represents a single primitive, which in our case (due to the aiProcess_Triangulate option) are always triangles. A face
contains the indices of the vertices we need to draw in what order for its primitive.
To retrieve the material of a mesh, we need to index the scene's mMaterials array. The mesh's material index is set in its
mMaterialIndex property, which we can also query to check if the mesh contains a material or not:
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());
}
Assimp to Mesh – Indices / Materials
The loadMaterialTextures function iterates over all the texture locations of the given texture type, retrieves the texture's file
location and then loads and generates the texture and stores the information in a Vertex struct.
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;
}
Note that we make the assumption that texture file paths in model files are local to the actual model object e.g. in the same
directory as the location of the model itself. We can then simply concatenate the texture location string and the directory string
we retrieved earlier (in the loadModel function) to get the complete texture path (that's why the GetTexture function also needs
the directory string).
Some models found over the internet use absolute paths for their texture locations, which won't work on each machine. In that
case you probably want to manually edit the file to use local paths for the textures (if possible).
Import 3D Models – An Optimization
Loading textures is not a cheap operation and in our current implementation a new texture is loaded and generated
for each mesh, even though the exact same texture could have been loaded several times before. This quickly
becomes the bottleneck of your model loading implementation.
• So we're going to add one small tweak to the model code by storing all of
the loaded textures globally.
• Wherever we want to load a texture, we first check if it hasn't been
loaded already.
• If so, we take that texture and skip the entire loading routine, saving us a
lot of processing power.
struct Texture {
unsigned int id;
string type;
string path; // we store the path of the texture to compare
with other textures
};
Then we store all the loaded textures in another vector declared at the
top of the model's class file as a private variable:
vector<Texture> textures_loaded;
Import 3D Models – An Optimization
In the loadMaterialTextures function, we want to compare the texture path with all the textures in the textures_loaded vector to
see if the current texture path equals any of those. If so, we skip the texture loading/generation part and simply use the located
texture struct as the mesh's texture. The (updated) function is shown below:
vector<Texture> loadMaterialTextures(aiMaterial *mat, …
aiTextureType type, string typeName) if(!skip)
{ { // if texture hasn't been loaded already, load it
vector<Texture> textures; Texture texture;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) texture.id = TextureFromFile(str.C_Str(), directory);
{ texture.type = typeName;
aiString str; texture.path = str.C_Str();
mat->GetTexture(type, i, &str); textures.push_back(texture);
bool skip = false; textures_loaded.push_back(texture); // add to loaded
for(unsigned int j = 0; j < textures_loaded.size(); j++) textures
{ }
if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) }
{ return textures;
textures.push_back(textures_loaded[j]); }
skip = true;
break; Some versions of Assimp tend to load models quite slow when using the
} debug version and/or the debug mode of your IDE, so be sure to test it
}
...
out with release versions as well if you run into slow loading times.
• The material and paths have been modified a bit so it works directly with the
way we've set up the model loading.
• The model is exported as a .obj file together with a .mtl file that links to the
model's diffuse, specular, and normal maps (review normal maps).
• Note that there's a few extra texture types we won't be using yet, and that all
the textures and the model file(s) should be located in the same directory for the
textures to load.
• The “modified” model is available on: backpack folder on virtual class page.
• It is recommended to only add the complete path to the model.
The modified version of the backpack uses local relative texture paths, and renamed
the albedo and metallic textures to diffuse and specular respectively.
Now, declare a Model object and pass in the model's file location. The model should then automatically load and (if
there were no errors) render the object in the render loop using its Draw function and that is it. No more buffer
allocations, attribute pointers, and render commands, just a simple one-liner.
Using Assimp you can load tons of models found over the internet. There are quite a few resource websites that offer free 3D
models for you to download in several file formats. Do note that some models still won't load properly, have texture paths
that won't work, or are simply exported in a format even Assimp can't read.
Import 3D Models – Exercise 16 Task 1
In this exercise the Survival Guitar Backpack model by Berk Gedik will be uploaded on OpenGL.
Video guide by Matthew Early on how to set up 3D models in Blender so they directly work with the current model
loader (as the texture setup we've chosen doesn't always work out of the box).
https://www.youtube.com/watch?v=4DQquG_o-Ac&ab_channel=Code%2CTech%2CandTutorials
Import 3D Models – Exercise 16 Task 2 - helicopter
In this exercise we will download a free model and import it on OpenGL.
.gltf
.obj + .mtl
DOWNLOAD
• Download model from https://sketchfab.com/feed, for example:
https://sketchfab.com/3d-models/hind-attack-helicopter-bb65bdfde2c54007a52dfbe1d91d930d
Some formats does not have default Textures uploaded on model. On this case, it is required to join
the Texture (Shading – Join) and add the textures connections. Please review:
https://www.youtube.com/watch?v=9NjMeAFkm3s&ab_channel=OnurComlekci
BLENDER EXPORT
• Export model (.obj) and edit .mtl file (delete full path file on textures).
• Rename .obj and .mtl files (optional).
• Copy the texture files (.jpg) on the same folder of .obj and .mtl files.
OPENGL IMPORT
• Update the model path on your OpenGL code.
• In case of wrong texture orientation, comment/uncomment the line: stbi_set_flip_vertically_on_load(true)
• Execute your application.
Import 3D Models – Exercise 16 Task 3 - baphomet
Source: https://sketchfab.com/3d-models/baphomet-151a369bf3c6417a988c282d71de9111
If the model is too big, adjust the model size using scale transformation.
If the speed is too slow, adjust the camera speed before render loop (camera.MovementSpeed = 10).