Qt and OpenGL: Loading a 3D Model with Open Asset Import Library (ASSIMP) - Part 2
Hello and welcome back. This is the follow-on to our original article, "Qt and OpenGL: Loading a 3D Model with Open Asset Import Library (ASSIMP)." Last time we covered how to use ASSIMP to load a model into our data structures such that is was ready for rendering with OpenGL. This time, we’re going to cover how to write a program to actually render these models.
We’re obviously going to Qt as our platform layer (it’s in this blog’s title) but we’re going to choose QOpenGLWindow from among all the various ways you can integrate OpenGL with Qt. (See this webinar for all the gory details: https://www.ics.com/webinars/state-art-opengl-and-qt).
#include <QtGui/QGuiApplication> #include <QtGui/QKeyEvent> #include <QtGui/QOpenGLWindow> #include <QtGui/QOpenGLBuffer> #include <QtGui/QOpenGLFunctions> #include <QtGui/QOpenGLShaderProgram> #include <QtGui/QOpenGLVertexArrayObject> #include "modelloader.h" static QString vertexShader = "#version 330 core\n" "\n" "layout(location = 0) in vec3 vertexPosition;\n" "layout(location = 1) in vec3 vertexNormal;\n" "\n" "out vec3 normal;\n" "out vec3 position;\n" "\n" "uniform mat4 MV;\n" "uniform mat3 N;\n" "uniform mat4 MVP;\n" " \n" "void main()\n" "{\n" " normal = normalize( N * vertexNormal );\n" " position = vec3( MV * vec4( vertexPosition, 1.0 ) );\n" " gl_Position = MVP * vec4( vertexPosition, 1.0 );\n" "}\n" ; static QString fragmentShader = "#version 330 core\n" "\n" "in vec3 normal;\n" "in vec3 position;\n" "\n" "layout (location = 0) out vec4 fragColor;\n" "\n" "struct Light\n" "{\n" " vec4 position;\n" " vec3 intensity;\n" "};\n" "uniform Light light;\n" "\n" "struct Material {\n" " vec3 Ka;\n" " vec3 Kd;\n" " vec3 Ks;\n" " float shininess;\n" "};\n" "uniform Material material;\n" "\n" "void main()\n" "{\n" " vec3 n = normalize( normal);\n" " vec3 s = normalize( light.position.xyz - position);\n" " vec3 v = normalize( -position.xyz);\n" " vec3 h = normalize( v + s);\n" " float sdn = dot( s, n);\n" " vec3 ambient = material.Ka;\n" " vec3 diffuse = material.Kd * max( sdn, 0.0);\n" " vec3 specular = material.Ks * mix( 0.0, pow( dot(h, n), material.shininess), step( 0.0, sdn));\n" " fragColor = vec4(light.intensity * (ambient + diffuse + specular), 1);\n" "}\n" ; struct Window : QOpenGLWindow, QOpenGLFunctions { Window() : m_vbo(QOpenGLBuffer::VertexBuffer), m_nbo(QOpenGLBuffer::VertexBuffer), m_ibo(QOpenGLBuffer::IndexBuffer) { } void createShaderProgram() { if ( !m_pgm.addShaderFromSourceCode( QOpenGLShader::Vertex, vertexShader)) { qDebug() << "Error in vertex shader:" << m_pgm.log(); exit(1); } if ( !m_pgm.addShaderFromSourceCode( QOpenGLShader::Fragment, fragmentShader)) { qDebug() << "Error in fragment shader:" << m_pgm.log(); exit(1); } if ( !m_pgm.link() ) { qDebug() << "Error linking shader program:" << m_pgm.log(); exit(1); } } void createGeometry() { } void initializeGL() { QOpenGLFunctions::initializeOpenGLFunctions(); createShaderProgram(); m_pgm.bind(); m_pgm.setUniformValue("light.position", QVector4D( -1.0f, 1.0f, 1.0f, 1.0f )); m_pgm.setUniformValue("light.intensity", QVector3D( 1.0f, 1.0f, 1.0f )); createGeometry(); m_view.setToIdentity(); m_view.lookAt(QVector3D(0.0f, 0.0f, 1.2f), // Camera Position QVector3D(0.0f, 0.0f, 0.0f), // Point camera looks towards QVector3D(0.0f, 1.0f, 0.0f)); // Up vector glEnable(GL_DEPTH_TEST); glClearColor(.9f, .9f, .93f ,1.0f); } void resizeGL(int w, int h) { glViewport(0, 0, w, h); m_projection.setToIdentity(); m_projection.perspective(60.0f, (float)w/h, .3f, 1000); update(); } void draw() { } void paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); m_pgm.bind(); m_vao.bind(); draw(); m_vao.release(); update(); } void keyPressEvent(QKeyEvent * ev) { if (ev->key() == Qt::Key_Escape) exit(0); } ModelLoader m_loader; QMatrix4x4 m_projection, m_view; QOpenGLShaderProgram m_pgm; QOpenGLVertexArrayObject m_vao; QOpenGLBuffer m_vbo, m_nbo; QOpenGLBuffer m_ibo; GLsizei m_cnt; }; int main(int argc, char *argv[]) { QGuiApplication a(argc,argv); QSurfaceFormat f; f.setMajorVersion( 3 ); f.setMinorVersion( 3 ); f.setProfile( QSurfaceFormat::CoreProfile ); Window w; w.setFormat(f); w.setWidth(800); w.setHeight(600); w.show(); return a.exec();
With this code we can pretty much draw any geometry you can give us, so our task then becomes how to fill in the empty createGeometry() method and the empty draw() method. Let’s tackle geometry creation first. If you recall that we ended our first blog post with this code:
ModelLoader model; if (!model.Load("head.3ds")) { m_error = true; return; } QVector*vertices; QVector *normals; QVector *indices; model.getBufferData(&vertices, &normals, &indices); m_rootNode = model.getNodeData();
And we said “At this point, you have all the data you need to display the model using OpenGL.” Let’s see how that translates into code.
void createGeometry() { if(!m_loader.Load("velociraptor_mesh_materials.dae", ModelLoader::RelativePath)) { qDebug() << "ModelLoader failed to load model" << m_pgm.log(); exit(1); } // Get the loaded model data from the model-loader: (v)ertices, (n)ormals, and (i)ndices QVector*v, *n; QVector *i; m_loader.getBufferData(&v, &n, &i); // Initialize and bind the VAO that's going to capture all this vertex state m_vao.create(); m_vao.bind(); // Put all the vertex data in a FBO m_vbo.create(); m_vbo.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_vbo.bind(); m_vbo.allocate(&(*v)[0], v->size() * sizeof((*v)[0])); // Configure the attribute stream m_pgm.enableAttributeArray(0); m_pgm.setAttributeBuffer(0, GL_FLOAT, 0, 3); // Put all the normal data in a FBO m_nbo.create(); m_nbo.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_nbo.bind(); m_nbo.allocate(&(*n)[0], n->size() * sizeof((*n)[0])); // Configure the attribute stream m_pgm.enableAttributeArray(1); m_pgm.setAttributeBuffer(1, GL_FLOAT, 0, 3); // Put all the index data in a IBO m_ibo.create(); m_ibo.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_ibo.bind(); m_ibo.allocate(&(*i)[0], i->size() * sizeof((*i)[0])); // Okay, we've finished setting up the vao m_vao.release(); }
What we’re doing here is using the ModelLoader object from our last blog post, asking it to load a “velociraptor” model, which then provides us with the geometry data in the std::vector<> objects. We then take that geometry data and create separate Vertex-Buffer and Index-Buffer objects on the graphics card and then upload that geometry data to the graphics card. From there, we configure the attribute streams for our vertex shader inputs as they point to these buffer objects and viola, we’re ready to render.
Now let’s tackle the actual rendering. Recall from the first article that the ModelLoader object returns a tree of mesh data with each mesh object containing a bunch of meta-data (including material color data) about how to render it. What we have to do is create an algorithm to traverse this “mesh-tree” rendering the meshes in order as we go. Let’s see how we do that in code.
void drawNode(const QMatrix4x4& model, const Node *node, QMatrix4x4 parent) { // Prepare matrices QMatrix4x4 local = parent * node->transformation; QMatrix4x4 mv = m_view * model * local; m_pgm.setUniformValue("MV", mv); m_pgm.setUniformValue("N", mv.normalMatrix()); m_pgm.setUniformValue("MVP", m_projection * mv); // Draw each mesh in this node for(int i = 0; imeshes.size(); ++i) { const Mesh& m = *node->meshes[i]; if (m.material->Name == QString("DefaultMaterial")) { m_pgm.setUniformValue("material.Ka", QVector3D( 0.05f, 0.2f, 0.05f )); m_pgm.setUniformValue("material.Kd", QVector3D( 0.3f, 0.5f, 0.3f )); m_pgm.setUniformValue("material.Ks", QVector3D( 0.6f, 0.6f, 0.6f )); m_pgm.setUniformValue("material.shininess", 50.f); } else { m_pgm.setUniformValue("material.Ka", m.material->Ambient); m_pgm.setUniformValue("material.Kd", m.material->Diffuse); m_pgm.setUniformValue("material.Ks", m.material->Specular); m_pgm.setUniformValue("material.shininess", m.material->Shininess); } glDrawElements(GL_TRIANGLES, m.indexCount, GL_UNSIGNED_INT, (const GLvoid*)(m.indexOffset * sizeof(GLuint))); } // Recursively draw this nodes children nodes for(int i = 0; i < node->nodes.size(); ++i) drawNode(model, &node->nodes[i], local); } void draw() { QMatrix4x4 model; model.translate(-0.2f, 0.0f, .5f); model.rotate(55.0f, 0.0f, 1.0f, 0.0f); drawNode(model, m_loader.getNodeData().data(), QMatrix4x4()); }
As you can see we’ve set up a recursive, decent visitor algorithm to traverse the mesh tree. Notice how at each recursive step that current level’s local transformation matrix becomes the next levels parent transformation matrix. Also, notice how we update our lighting parameters with each mesh’s lighting meta-data in order render that mesh correctly.
And that’s it, we’re rendering velociraptors with Qt, OpenGL and ASSIMP!