How to load and draw 3D models with ASSIMP?

Assimp模型库结构剖析与使用

Posted by Tao on Saturday, March 19, 2022

本篇是在使用assimp模型库的过程中发现的一篇教程,该文对ASSIMP介绍较为全面,而网络上与之相关的文章较少,故记录之。该教程名为Interactive Computer Graphics,本篇是其第16章,该教程现已不可考,本篇仅做记录,如涉及侵权请联系我删除:yzthr@hotmail.com

Loading and displaying 3D models using Assimp

In the examples we discussed so far, most of the 3D models are very simple and are embedded in the source code. If you want to load and display 3D models from external files, you will need to use a 3D loader such as ASSIMP. OpenGL does not provide any API for loading 3D models. ASSIMP is an open source C++ library for loading various 3D files, such as *.fbx,*.dae,*.obj,*.blend, *.3ds, etc.

The typical process of drawing a 3D model with ASSIMP include:

  • Create a 3D model in a 3D modeling tool, such as Maya, 3DS Max, or Blender.
  • Store the 3D model in a 3D model file, such as Obj, or Collada.
  • Load the 3D model using the Assimp library. The 3D model is now stored in Assimp’s hierarchical C++ data structures.
  • Transfer the 3D model data to a shader program.
  • Draw the 3D object.

How is a 3D model stored in a 3D file?

Here is an example. A simple box object (see figure) created in Blender is exported to an Obj file and a Collada file.

Here is the Obj file of the box object shown above. To keep it simple, this file doesn’t contain normal vectors and texture coordinates. box

# Blender v2.71 (sub 0) OBJ File: ''
# www.blender.org
mtllib simple_box.mtl
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
usemtl Material
s off
f 2 3 4
f 8 7 6
f 1 5 6
f 2 6 7
f 7 8 4
f 1 4 8
f 1 2 4
f 5 8 6
f 2 1 6
f 3 2 7
f 3 7 4
f 5 1 8

Here is the Collada file of the box object shown above. The Collada file format is more complicated than the OBJ file format. A Collada file contains more information, such as the the hierarchical relationship of the objects. This hierarchical relationship is stored in the node graph inside Assimp’s aiScene object.

<?xml version="1.0" encoding="utf-8"?>
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
  <asset>
    <contributor>
      <author>Blender User</author>
      <authoring_tool>Blender 2.71.0 commit date:2014-06-25, commit time:18:36, hash:9337574</authoring_tool>
    </contributor>
    <created>2014-09-23T10:42:50</created>
    <modified>2014-09-23T10:42:50</modified>
    <unit name="meter" meter="1"/>
    <up_axis>Z_UP</up_axis>
  </asset>
  <library_cameras>
    <camera id="Camera-camera" name="Camera">
      <optics>
        <technique_common>
          <perspective>
            <xfov sid="xfov">49.13434</xfov>
            <aspect_ratio>1.777778</aspect_ratio>
            <znear sid="znear">0.1</znear>
            <zfar sid="zfar">100</zfar>
          </perspective>
        </technique_common>
      </optics>
      <extra>
        <technique profile="blender">
          <YF_dofdist>0</YF_dofdist>
          <shiftx>0</shiftx>
          <shifty>0</shifty>
        </technique>
      </extra>
    </camera>
  </library_cameras>
  <library_lights>
    <light id="Lamp-light" name="Lamp">
      <technique_common>
        <point>
          <color sid="color">1 1 1</color>
          <constant_attenuation>1</constant_attenuation>
          <linear_attenuation>0</linear_attenuation>
          <quadratic_attenuation>0.00111109</quadratic_attenuation>
        </point>
      </technique_common>
      <extra>
        <technique profile="blender">
          <adapt_thresh>0.000999987</adapt_thresh>
          <area_shape>1</area_shape>
          <area_size>0.1</area_size>
          <area_sizey>0.1</area_sizey>
          <area_sizez>1</area_sizez>
          <atm_distance_factor>1</atm_distance_factor>
          <atm_extinction_factor>1</atm_extinction_factor>
          <atm_turbidity>2</atm_turbidity>
          <att1>0</att1>
          <att2>1</att2>
          <backscattered_light>1</backscattered_light>
          <bias>1</bias>
          <blue>1</blue>
          <buffers>1</buffers>
          <bufflag>0</bufflag>
          <bufsize>2880</bufsize>
          <buftype>2</buftype>
          <clipend>30.002</clipend>
          <clipsta>1.000799</clipsta>
          <compressthresh>0.04999995</compressthresh>
          <dist sid="blender_dist">29.99998</dist>
          <energy sid="blender_energy">1</energy>
          <falloff_type>2</falloff_type>
          <filtertype>0</filtertype>
          <flag>0</flag>
          <gamma sid="blender_gamma">1</gamma>
          <green>1</green>
          <halo_intensity sid="blnder_halo_intensity">1</halo_intensity>
          <horizon_brightness>1</horizon_brightness>
          <mode>8192</mode>
          <ray_samp>1</ray_samp>
          <ray_samp_method>1</ray_samp_method>
          <ray_samp_type>0</ray_samp_type>
          <ray_sampy>1</ray_sampy>
          <ray_sampz>1</ray_sampz>
          <red>1</red>
          <samp>3</samp>
          <shadhalostep>0</shadhalostep>
          <shadow_b sid="blender_shadow_b">0</shadow_b>
          <shadow_g sid="blender_shadow_g">0</shadow_g>
          <shadow_r sid="blender_shadow_r">0</shadow_r>
          <sky_colorspace>0</sky_colorspace>
          <sky_exposure>1</sky_exposure>
          <skyblendfac>1</skyblendfac>
          <skyblendtype>1</skyblendtype>
          <soft>3</soft>
          <spotblend>0.15</spotblend>
          <spotsize>75</spotsize>
          <spread>1</spread>
          <sun_brightness>1</sun_brightness>
          <sun_effect_type>0</sun_effect_type>
          <sun_intensity>1</sun_intensity>
          <sun_size>1</sun_size>
          <type>0</type>
        </technique>
      </extra>
    </light>
  </library_lights>
  <library_images/>
  <library_effects>
    <effect id="Material-effect">
      <profile_COMMON>
        <technique sid="common">
          <phong>
            <emission>
              <color sid="emission">0 0 0 1</color>
            </emission>
            <ambient>
              <color sid="ambient">0 0 0 1</color>
            </ambient>
            <diffuse>
              <color sid="diffuse">0.64 0.64 0.64 1</color>
            </diffuse>
            <specular>
              <color sid="specular">0.5 0.5 0.5 1</color>
            </specular>
            <shininess>
              <float sid="shininess">50</float>
            </shininess>
            <index_of_refraction>
              <float sid="index_of_refraction">1</float>
            </index_of_refraction>
          </phong>
        </technique>
      </profile_COMMON>
    </effect>
  </library_effects>
  <library_materials>
    <material id="Material-material" name="Material">
      <instance_effect url="#Material-effect"/>
    </material>
  </library_materials>
  <library_geometries>
    <geometry id="Cube-mesh" name="Cube">
      <mesh>
        <source id="Cube-mesh-positions">
          <float_array id="Cube-mesh-positions-array" count="24">1 1 -1 1 -1 -1 -1 -0.9999998 -1 -0.9999997 1 -1 1 0.9999995 1 0.9999994 -1.000001 1 -1 -0.9999997 1 -1 1 1</float_array>
          <technique_common>
            <accessor source="#Cube-mesh-positions-array" count="8" stride="3">
              <param name="X" type="float"/>
              <param name="Y" type="float"/>
              <param name="Z" type="float"/>
            </accessor>
          </technique_common>
        </source>
        <source id="Cube-mesh-normals">
          <float_array id="Cube-mesh-normals-array" count="36">0 0 -1 0 0 1 1 -5.66244e-7 3.27825e-7 -4.76837e-7 -1 0 -1 2.08616e-7 -1.19209e-7 2.08616e-7 1 2.38419e-7 0 0 -1 0 0 1 1 0 -2.38419e-7 0 -1 -2.98023e-7 -1 2.38419e-7 -1.49012e-7 2.68221e-7 1 1.78814e-7</float_array>
          <technique_common>
            <accessor source="#Cube-mesh-normals-array" count="12" stride="3">
              <param name="X" type="float"/>
              <param name="Y" type="float"/>
              <param name="Z" type="float"/>
            </accessor>
          </technique_common>
        </source>
        <vertices id="Cube-mesh-vertices">
          <input semantic="POSITION" source="#Cube-mesh-positions"/>
        </vertices>
        <polylist material="Material-material" count="12">
          <input semantic="VERTEX" source="#Cube-mesh-vertices" offset="0"/>
          <input semantic="NORMAL" source="#Cube-mesh-normals" offset="1"/>
          <vcount>3 3 3 3 3 3 3 3 3 3 3 3 </vcount>
          <p>0 0 1 0 2 0 7 1 6 1 5 1 4 2 5 2 1 2 5 3 6 3 2 3 2 4 6 4 7 4 4 5 0 5 3 5 3 6 0 6 2 6 4 7 7 7 5 7 0 8 4 8 1 8 1 9 5 9 2 9 3 10 2 10 7 10 7 11 4 11 3 11</p>
        </polylist>
      </mesh>
    </geometry>
  </library_geometries>
  <library_controllers/>
  <library_visual_scenes>
    <visual_scene id="Scene" name="Scene">
      <node id="Camera" name="Camera" type="NODE">
        <matrix sid="transform">0.6858805 -0.3173701 0.6548619 7.481132 0.7276338 0.3124686 -0.6106656 -6.50764 -0.01081678 0.8953432 0.4452454 5.343665 0 0 0 1</matrix>
        <instance_camera url="#Camera-camera"/>
      </node>
      <node id="Lamp" name="Lamp" type="NODE">
        <matrix sid="transform">-0.2908646 -0.7711008 0.5663932 4.076245 0.9551712 -0.1998834 0.2183912 1.005454 -0.05518906 0.6045247 0.7946723 5.903862 0 0 0 1</matrix>
        <instance_light url="#Lamp-light"/>
      </node>
      <node id="Cube" name="Cube" type="NODE">
        <matrix sid="transform">1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1</matrix>
        <instance_geometry url="#Cube-mesh">
          <bind_material>
            <technique_common>
              <instance_material symbol="Material-material" target="#Material-material"/>
            </technique_common>
          </bind_material>
        </instance_geometry>
      </node>
    </visual_scene>
  </library_visual_scenes>
  <scene>
    <instance_visual_scene url="#Scene"/>
  </scene>
</COLLADA>

How is the 3D model data stored in Assimp’s data structure?

The 3D model file is loaded into Assimp’s data structure through the following function

scene = importer.ReadFile(pFile, aiProcessPreset_TargetRealtime_Quality);

If successful, the 3D data are stored in the following hierarchical Assimp data structures.

  • aiScene
    • mNumMeshes (number of meshes for the entire scene)
    • mMeshes (array of meshes)
      • mNumFaces (number of faces)
      • mFaces (array of faces, usually triangles)
        • mNumIndices (number of vertex indices per face, usually 3)
        • mIndices (array of vertex indices, also called elements)
      • mNumVertices (number of vertices for this mesh)
      • mVertices (vertex array. Each vertex is a 3 dimensional vector.)
      • mNormals (array of normals, aligned with the vertex array)
      • mTextureCoords (array of texture coordinates, aligned with the vertex array)
      • mMaterialIndex (the index of the material associated with this mesh)
    • mNumMaterials (number of materials in the scene)
    • mMaterials (array of materials)
      • GetTextureCount() (get the number of texture attached to this material)
      • GetTexture() (get the path of the texture files attached to this material)
    • mRootNode (root node of the scene graph)
      • mNumChildren (the number of children for this node)
      • mChildren (an array of child nodes)
        • mNumChildren (the number of child nodes for this node)
        • mChildren (an array of child nodes)
        • mTransformation (transformation matrix for this node)
        • mNumMeshes (number of meshes associated with this node)
        • mMeshes (an array of the indices of the meshes associated with this node)
        • (More child nodes may be added.)

Figure 1 and 2 show where different parts of the 3D file are stored in different parts of the aiScene data structure.
figure1 Figure 1. This figure shows where vertex positions, vertex normals, vertex texture coordinates, and face indices are stored in different member variables in Assimp’s aiScene object. figure2 Figure 2. This figure shows where material properties and transformation matrices are stored in Assimp’s aiScene object. The curved arrows shows how different member variables in aiScene are connected.

It is important to note that during post-processing, Assimp may reorganize the face index array, vertex array, normal array, and texture coordinate array. The 3D data you find in the aiScene data structure may not be the same as the data you find in the 3D file. For example, a cube object has 8 vertices in the OBJ file. After this OBJ file is loaded by Assimp, there are 24 vertices in the vertex position array.

Assimp data structure data_structure

How to transfer 3D data from Assimp’s data structures to GPU?

OpenGL is a C library, but Assimp is a C++ library. Therefore some of the 3D data stored in Assimp’s C++ objects need to be copied to “flat” C arrays so that they can be used in OpenGL functions.

The following code segment is taken from the sample program l3dassimpimport.cpp, under the function genVAOsAndUniformBuffer(const aiScene *sc).

Transfer face indices (elements) to GPU

The following code segment copy the face indices from Assimp’s aiMesh object to a one dimensional array faceArray.

// For each mesh in the aiScene object
for (unsigned int n = 0; n < sc->mNumMeshes; ++n)
{
    // Get the current aiMesh object.
    const aiMesh *mesh = sc->mMeshes[n];

    // Create a one dimensional array to store face indices
    unsigned int *faceArray;
    faceArray = (unsigned int *)malloc(sizeof(unsigned int) * mesh->mNumFaces * 3);
    unsigned int faceIndex = 0;

    // Copy face indices from aiMesh to faceArray.
    // In an aiMesh object, face indices are stored in two layers. There is an array of aiFace, and then each aiFace stores 3 indices.
    // Must copy face indices to a one dimensional array so that it can be used in OpenGL functions.
    for (unsigned int t = 0; t < mesh->mNumFaces; ++t)
    {
        const aiFace *face = &mesh->mFaces[t]; // Go through the list of aiFace

        // For each aiFace, copy its indices to faceArray.
        memcpy(&faceArray[faceIndex], face->mIndices, 3 * sizeof(unsigned int));
        faceIndex += 3;
    }
    aMesh.numFaces = sc->mMeshes[n]->mNumFaces; // Record the number of faces.

The following code segment transfers the face index array (faceArray) to GPU.

// Generate and bind a vertex buffer object for face indices (also called elements)
glGenBuffers(1, &buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
// Transfer the faceArray to the vertex buffer object.
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * mesh->mNumFaces * 3, faceArray, GL_STATIC_DRAW);

Transfer vertex positions to GPU

The following code segment transfer vertex position to a vertex buffer object.

if (mesh->HasPositions())
{
    // Generate a Vertex Buffer Object (VBO) for vertex positions
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // Transfer the vertex position array (stored in aiMesh's member variable mVertices) to the VBO.
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * mesh->mNumVertices, mesh->mVertices, GL_STATIC_DRAW);

    // Link this VBO with a variable in the vertex shader.
    // Each VBO must be linked with a "in" variable in the vertex shader.
    glEnableVertexAttribArray(vertexLoc);
    glVertexAttribPointer(vertexLoc, 3, GL_FLOAT, 0, 0, 0);
}

Transfer vertex normals to GPU

The normals are used mainly for lighting calculations. The following code segment transfer vertex normals to a vertex buffer object.

if (mesh->HasNormals())
{
    // Generate a Vertex Buffer Object (VBO) for vertex normals
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // Transfer the vertex position array (stored in aiMesh's member variable mNormals) to the VBO.
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * mesh->mNumVertices, mesh->mNormals, GL_STATIC_DRAW);

    // Link this VBO with a variable in the vertex shader.
    // Each VBO must be linked with a "in" variable in the vertex shader.
    glEnableVertexAttribArray(normalLoc);
    glVertexAttribPointer(normalLoc, 3, GL_FLOAT, 0, 0, 0);
}

Transfer texture coordinates to GPU

Note that in Assimp the texture coordinates are not stored in a “flat” array. Therefore the texture coordinates must be copied to a one-dimensional array so that it can be used in OpenGL functions.

if (mesh->HasTextureCoords(0))
{ // Assuming only one texture is attached to this mesh

    float *texCoords = (float *)malloc(sizeof(float) * 2 * mesh->mNumVertices);
    for (unsigned int k = 0; k < mesh->mNumVertices; ++k)
    {
        // Go through the array of texture coordinates and copy them to a one-dimensional array.
        // Note that in Assimp the texture coordinates are not stored in a "flat" array.
        // The mTextureCoords variable in aiMesh is a 2D array.
        // The first dimension is the number of textures associated with this mesh.
        // The second dimension is the number of vertices.
        // Assimp uses aiVector3D to store texture coordinates, but a texture coordinate only uses the first two components.
        texCoords[k * 2] = mesh->mTextureCoords[0][k].x;
        texCoords[k * 2 + 1] = mesh->mTextureCoords[0][k].y;
    }

The following code segment transfer vertex texture coordinates to a vertex buffer object.

    // Generate a Vertex Buffer Object (VBO) for vertex texture coordinates
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);

    // Transfer the texture coordinate array to the VBO.
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 2 * mesh->mNumVertices, texCoords, GL_STATIC_DRAW);

    // Link this VBO with a variable in the vertex shader.
    // Each VBO must be linked with a "in" variable in the vertex shader.
    glEnableVertexAttribArray(texCoordLoc);
    glVertexAttribPointer(texCoordLoc, 2, GL_FLOAT, 0, 0, 0);
}

How to draw 3D objects?

The drawing of a 3D object is guided by the node tree inside the aiScene object because the node tree describes the hierarchical relationship of the different meshes of the 3D object. A typical process of drawing a 3D object is as follows:

  • Start with the aiScene object. Find the root node of the node tree.

  • From the root node, traverse the tree. Every node is visited during the tree traversal. Some nodes have a list meshes attached to it. In this case, the node has an array that stores the indices of the meshes stored in the aiMesh array in the aiScene object. Some nodes have no mesh attached. Then there is nothing to draw for these nodes.

  • For a node that has meshes attached to it, iterate through each mesh and find the index into the aiMesh array in the aiScene object. In order to draw the mesh, the program needs to find the index of the Vertex Buffer Object (VAO) that contains the mesh. Note that every mesh in the aiMesh array of the aiScene object has already been stored in a VAO.

  • How to use the index of the mesh in the aiMesh array to find the index of the corresponding VAO? In the sample program, this is handled by creating an array of MyMesh struct. The MyMesh array is perfectly aligned with the aiMesh array in the aiScene object. That is, the first mesh in the aiMesh array is the the first mesh in the MyMesh array, and so on. After a mesh in the aiMesh array is saved in a VAO, the VAO index is stored in the corresponding MyMesh struct. Because the MyMesh array is aligned with the aiMesh array in the aiScene object, the program can use the index of the mesh in the aiMesh array to find the corresponding MyMesh struct, in which the program can find the index to the corresponding VAO.

  • With the index of the VAO, the program can draw the mesh using OpenGL functions.

  • After all meshes attached to one node is drawn, the program continue the traversal and visit the next node.

  • When all the nodes are visited, all the meshes attached to this 3D object are drawn.
    The following code segment shows how to traverse the node tree in a aiScene object and draw the 3D meshes attached to each node. It is important to understand how each node is linked with the Vertex Array Objects that contains the meshes attached to that node.

For a more complete example, please see the attached programs.

void recursive_render(const aiScene *sc, const aiNode *nd)
{
    // draw all meshes assigned to this node
    for (unsigned int n = 0; n < nd->mNumMeshes; ++n)
    {
        // Bind VAO, which contains the VBOs for indices, positions, normals, and texture coordinates
        // of the current mesh for the current node.
        // Each node in aiScene has an index (nd->mMeshs[n]) into the aiMesh array (see Figure 2).
        // In previous steps, each aiMesh in the aiMesh array has been transferred to a Vertex Array Object (VAO).
        // The indices of these VBOs are stored in an array myMeshes[].
        // The aiMesh array in an aiScene object and the myMeshes[] array is perfectly aligned.
        // That is, the first mesh in the aiMesh list corresponds to the first mesh on the myMeshes[] array, and so on.
        // Therefore, myMeshes[0].vao is the index of the VAO for the first aiMesh in the aiScene object, and so on.
        glBindVertexArray(myMeshes[nd->mMeshes[n]].vao);

        // Use glDrawElements() to draw the 3D object because we have an index buffer in the VAO.
        glDrawElements(GL_TRIANGLES, myMeshes[nd->mMeshes[n]].numFaces * 3, GL_UNSIGNED_INT, 0);
    }

    // Draw all THE children
    for (unsigned int n = 0; n < nd->mNumChildren; ++n)
    {
        recursive_render(sc, nd->mChildren[n]);
    }
}

// Rendering Callback Function

void renderScene(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Specify a particular shader program object to draw the next object.
    glUseProgram(program);

    // Traverse the node graph in the aiScene object and draw the meshes attached to each node.
    // Some nodes may not have mesh attached.
    recursive_render(scene, scene->mRootNode);

    // Swap front and back color buffers so that the image is displayed.
    glutSwapBuffers();
}

assimp_utilities.hpp (12k) check_error.hpp (6k)

「如果这篇文章对你有用,请随意打赏」

Heisenberg Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付


comments powered by Disqus