Modern design 3D model file into opengl display

Disclaimer: This article is a blogger original article, shall not be reproduced without the bloggers allowed. https://blog.csdn.net/leon_zeng0/article/details/88957824

The use of 3D model import library assimp, you can read a wide variety of 3D model file format, now we will use opengl displayed. Which requires a bridge, is the data and opengl show, we introduced earlier Mesh, class, and now introduce the model class. With these two categories, we will be able to complete the display of the 3D model.

In this article the main reference  https://learnopengl.com/   and  https://learnopengl-cn.github.io/  learn from.

This procedure reduced the visual studio 2017 by the formula. Take a look at operating results:

The main reason why only in 2017 by the visual studio, my assimp compiled only in 2017 under visual studio through.

Before watching this article, you need to be ready assimp library, modern design assimp 3D model opengl to load the library  describes how to compile assimp library.

This article also uses the Mesh class, if you want a closer look look  modern opengl design, model import mesh class , this article is to do a kind of preparation for this article.

Model class

Before introducing the first structure of the Model class for you:

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);
};

Model class contains a Mesh object vector, constructors we need to give it a file path. In the constructor, it will directly load files by loadModel. Assimp handler will handle part of the import process, we will soon introduce them. We need to store the file directory path, when it will be used after loading textures.

Draw function is nothing special, basically through all the grid, and call their respective Draw function.

void Draw(Shader shader)
{
    for(unsigned int i = 0; i < meshes.size(); i++)
        meshes[i].Draw(shader);
}

OpenGL 3D model introduced into

To import a model, and convert it into our own data structures, first of all we need to include Assimp corresponding header files.

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

By introducing model loadModel, this code is called directly from the constructor. In loadModel, we use the model to load Assimp Assimp called to a scene data structure. You may remember in modern design assimp 3D model opengl to load the library , which is the root object Assimp data interface. Once we have this scene objects, we will be able to access model data is loaded in all required.

Assimp great thing is that it abstracts away all the technical details of the load different file formats, only one line of code will be able to do all the work:

Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

We first declare a namespace Importer in Assimp, after calling its ReadFile function. This function requires a file path, its second parameter is the number of options for post-processing (Post-processing) of. In addition to loading files, Assimp allows us to set some options to force it to import the data to do some additional calculations or operations. By setting aiProcess_Triangulate, we told Assimp, if the model is not (all) composed of triangles, it needs all the primitive shape model into a triangle. aiProcess_FlipUVs texture coordinates when dealing with flip y-axis (you may remember we textures said tutorial, in most of the OpenGL y-axis images are the opposite, so this post-processing option will fix this) . Other useful options are:

  • aiProcess_GenNormals: If the model does not include the normal vector, then it is normal to each vertex is created.
  • aiProcess_SplitLargeMeshes: the larger grid is divided into smaller sub-grids, if you have the maximum number of vertices to render restrictions, can only render a small grid, then it will be very useful.
  • aiProcess_OptimizeMeshes: and the last option is the opposite, it will splice multiple small meshes into one large grid, thereby reducing draw calls to be optimized.

Assimp provide a lot of useful post-processing instructions, you can here find all the instructions. Actually use Assimp load model is very easy (you can see). The difficulty is returned after use scene object will load data into a Mesh object into an array.

Complete loadModel function will look like this:

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);
}

After we loaded the model, we will check the scene and its root is not null, and check out one of its mark (Flag), to see the data returned is not incomplete. If you encounter any errors, we will report errors via the introducer GetErrorString function and return. We also get the directory path of the file path.

If something goes wrong did not happen, we want to handle all the nodes in the scene, so we will first node (root) passed a recursive function processNode. Because each node (might) contain more than one child node, we want to first deal with the parameters of the node, and then continue to deal with all the children of the node, and so on. This is in line with a recursive structure, so we will define a recursive function. Recursive function after doing some processing, using different parameters recursive call to the function itself until some condition is met to stop the recursion. Exit conditions (Exit Condition) in our case is that all nodes are finished processing.

You may remember Assimp structure, each node contains a series of grid indexes, each grid point to that particular scene object. Then we wanted to go get these grid index, get each grid, each grid processing, and then repeat this process for each node of the child node. SUMMARY processNode function is as follows:

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);
    }
}

First check grid index for each node (loop), acquires the corresponding grid. Returns sent to the grid mesh processMesh function, it will return a Mesh object, the storage Mesh (push_back) in Meshes (a vector) of the.

After all the grid have been processed, then all the child nodes of node traversal, and they call the same functions processMesh. When a node no longer has any child nodes, this function is executed.

Serious readers may find that we can basically forget about any processing nodes, only need to traverse all mesh objects in the scene, you do not need to do this a bunch of index complex things. The reason we do so is still using the node initial idea is to define a parent-child relationship between the mesh. By recursively traverse this relationship, we will be able to define a grid to another grid of the grid parent.
A case of using this system is that when you want to shift a car grid, you can ensure that all of its sub-grids (such as engine grid, grid steering wheel, tire grid) will together with displacement. Such a system can easily create it with the parent-child relationship.

Now, however, we did not use such a system, but if you want to have more control over your grid data are usually recommended to use this kind of method. After all, this kind of relationship between the node is defined by the artist created this model.

The next step is to resolve Assimp data to the modern opengl design, model import mesh type Mesh class created.

In this function call is completed, the end of the import of scope, it occupies the resources released automatically. Because the data we need to have a mesh class.

From Assimp to Grid

An aiMeshobject into our own mesh object is not so difficult. We have to do is visit the grid-related attributes and store them into our own object. processMesh general structure functions as follows:

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);
}

During the processing of the grid has three main parts: an overview of all the vertex data, obtain their grid index, and to obtain the relevant data material. The processed data will be stored in three vector which, we will use them to build a Mesh object and returns it to the caller of the function there.

Acquires vertex data is very simple, we define a Vertex structure, after each iteration we will add it to the vertices array. We will traverse all the vertices (using the grid mesh->mNumVerticesto get). In each iteration, we want to use all the data to populate this structure. The position of the vertex is treated:

glm::vec3 vector; 
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z; 
vertex.Position = vector;

We note that in order to transmit data Assimp, we define a vec3temporary variable. The reason for using such a temporary variable is Assimp of vectors, matrices, etc. have their own set of string data types, and they are not perfectly converted to the data type of GLM.

Assimp it's called the vertex positions array mVertices, this is actually not so intuitive.

The normal processing steps are also similar:

vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;

Processing the texture coordinates are also broadly similar, but Assimp model allows a maximum of eight different texture coordinates on a vertex, we will not use that much, we only care about the first set of texture coordinates. We also wanted to check whether the grid really contains texture coordinates (probably not been the case)

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 structure is reflected in the vertex attributes have been populated need, we will finally pressed into vertices of this vector iteration of the tail. This process will mesh vertices are each repeat.

index

Assimp interface defines each grid has a surface (Face) arrays, each plane represents a primitive, in our example (aiProcess_Triangulate option since a) it is always triangular. A surface comprising a plurality of indexes, each of which is defined in the primitive, the vertex of which we should draw, drawing and in what order, so if we through all surfaces, and stores the index of a face to the vector indices on it.

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]);
}

All external cycle is over, we now have a series of vertex and index data, they can be used to draw the grid by glDrawElements function. However, to the end of this topic, and provide some details on the grid, we also need to address the mesh material.

Material

And nodes, a mesh index contains only a pointer to the material object. If you really want to get the mesh material, we also need to mMaterials array index scene. Mesh material is in its mMaterialIndex index attribute, we can also use it to detect whether a grid material comprises:

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());
}

We first acquired from mMaterials array of scene aiMaterialobjects. Next we want to load grid diffuse reflection and / or specular light map. An internal texture of the material for each object type are stored a texture position in the array. Different texture types with aiTextureType_prefixed. We use a tool called loadMaterialTextures function to get the texture from the material. This function will return a vector Texture structure, we will store it after the tail textures vector model.

loadMaterialTextures function traverses all textures to a given position of the texture type, the texture of the acquired file location, and load and generation and texture, the information is stored in a Vertex structure. It looks like this:

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;
}

We in the number of texture of the material stored in the first inspection by GetTextureCount function, this function requires a type of texture. We will use GetTexture get the location of each file texture, it will result in a storage aiStringmedium. We then use a tool called the TextureFromFile another function, it will (a stb_image.h) to load a texture and returns the texture ID. If you are not sure how this code is written, you can see the last of the complete code.

Note that we assume the path model file texture file is relative to the local (Local) model file path, such as the model file in the same directory. We can be spliced ​​into the texture of the string before (in loadModel in) the acquisition of directory string to get the full texture path (which is why GetTexture function also requires a directory string).

Some models have a texture position found on the network using the absolute (Absolute) path, which can not all work on each machine. In this case, you may need to manually modify this file, to make it the texture using a local path (if possible).

This is using all Assimp the imported model.

Major optimization

It has not completely ended, because we want to make a major (but not entirely necessary) optimization. Most part of the scene will be reused in the plurality of texture grid. Or think about a house, its walls with granite texture. The texture can also be applied to floors, ceilings, stairs, tables, and even a well nearby. Load the texture is not a little expensive operation, in our current implementation, even if the same texture has been loaded too many times, it will still load and generate a new texture for each grid. It will soon become a performance bottleneck model loads to achieve.

So we have a model to adjust the code, all the textures loaded through the global storage, whenever we want to load a texture, first to check that it has not been loaded before. If so, we will directly use that texture, and skip the entire loading process, to save a lot of processing power for us. In order to be able to compare the texture, we also need to store their path:

struct Texture {
    unsigned int id;
    string type;
    aiString path;  // 我们储存纹理的路径用于与其它纹理进行比较
};

Next we had to load all textures are stored in another vector in a statement at the top of the model class to a private variable:

vector<Texture> textures_loaded;

After that, loadMaterialTextures function, we hope that all textures textures_loaded this vector in the path of texture and storage are compared to see if the current path with a texture which is the same. If so, skip portions of texture load / generated directly used to locate the texture structure is a mesh texture. The update function is as follows:

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;
}

So now we not only have a flexible model loading system, we also received a load objects quickly optimized version.

Some versions of Assimp load the model in use or use the debug version of the IDE debug mode will be very slow, so when you encounter slow loading speed, you can try using a release.

You can here find the complete source code optimized Model class.

And bid farewell to the box model

So, let's import a model created by a real artist, and I substitute this work of genius (you have to admit, these boxes may be the most beautiful you've seen the cube), to test our implementation of it. Because I do not want too much credit my account, I would occasionally let other artists to join us, this time we will load the original game Crysis Crytek (Crysis) in nano-loaded (Nanosuit). This model is output as a .objfile and a .mtlfile, .mtlthe file contains a diffuse reflection model, light mirror normal map (this will be to learn later), you can here be downloaded to the model (after slightly modified), note that all texture and model files should be located in the same directory, for loading textures.

note

The basic subject matter referenced above website, in the debugging process, the following points should be noted:

1: in the main program

Model ourModel (FileSystem :: getPath ( "resources / objects / nanosuit / nanosuit.obj")); This line needs to be modified to your actual directory, such as

Model ourModel("D:\\study17\\nanosuit\\nanosuit.obj");  

2: In model.h file, directory have been the line of code need to be modified into the following:

directory = path.substr(0, path.find_last_of('\\'));

At first I can run, but a dark, I debug to this line, find the value of their directory are not needed.

3: The model provides perfect display, but not all of this can be primarily some file is incomplete, such as obj need .mtl pictures and other texture files. No texture files can not be displayed.

4: using the teachings herein may be transplanted to the 3D model introduced in the MFC interface.

 

Guess you like

Origin blog.csdn.net/leon_zeng0/article/details/88957824