Qt and OpenGL

Qt and OpenGL: Loading a 3D Model with Open Asset Import Library (ASSIMP) - Part 2

By ICS Development Team

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).

 

 

 

Here’s a listing for our skeleton project:
#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!

Velociraptor opengl 3d