Intro to OpenGL

Wednesday | Friday
OpenGL
Last week we saw a quick overview of the OpenGL pipeline. This week we will extend the shaders and MyPanelOpenGL class to add more functionality to our program.

A recap of the process for creating an OpenGL application in QT.

  1. Create a new class, e.g., MyPanelOpenGL which inherits from QOpenGLWidget
  2. Create Data on the GPU using Vertex Buffer Object (VBOs)
  3. Compile/Link Shader programs that will run on the GPU using data from the VBO
  4. Implement the methods initializeGL(), paintGL() and resizeGL ( int width, int height ) in your MyPanelOpenGL class to draw the scene

Setup

Open up two terminals. In the first, navigate to the qtogl folder in your build directory and run the application. You should see a red triangle.
[~]$ cd ~/cs40/examples/build/
[build]$ cd w01-intro/qtogl/
[qtogl]$ ./qtogl
In the second terminal, navigate to the qtogl folder in your source directory to edit files
[~]$ cd ~/cs40/examples/
[example]$ cd w01-intro/qtogl/
[qtogl]$ ls
CMakeLists.txt  mainwindow.cpp  mainwindow.ui      mypanelopengl.h
main.cpp        mainwindow.h    mypanelopengl.cpp  shaders/

Shaders, GLSL

Modifying Shaders

Let's modify our shaders to see how changes effect our program.
geometry displacement
Modify your shader/vshader.glsl to be the following
#version 410

in vec4 vPosition;

void
main()
{  
    vec4 disp = vec4(0., 0.2, 0., 0.); 
    gl_Position =  vPosition + disp;
}
Since shaders are compiled at runtime, you do not need to run make. Just run the ./qtogl application in the build terminal. The triangle should be shifted up slightly.
Using uniforms
A uniform variable is a constant variable for all parallel executions of the shaders. Recall that a vertex shader runs in parallel over all vertices in a VBO. The shader describes what should happen for one vertex and OpenGL handles changing the value of vPosition for each vertex in the VBO. The fragment shader runs in parallel for each potential output pixel. OpenGL manages setting the location of each fragment on the screen.

We can declare our displacement to be a uniform variable using the shader below for shader/vshader.glsl

#version 410

in vec4 vPosition;
uniform vec4 disp;

void
main()
{  
    gl_Position =  vPosition + disp;
}
We can set the value of the uniform in the MyPanelOpenGL::paintGL() method in myopenpanel.cpp
void MyPanelOpenGL::paintGL(){

    ...

    shaderProgram->enableAttributeArray("vPosition");
    shaderProgram->setAttributeBuffer("vPosition", GL_FLOAT, 0, 4, 0);
    shaderProgram->setUniformValue("disp",QVector4D(0.,0.2, 0.,0.));
    glDrawArrays(GL_TRIANGLES, 0, numVertices);

    ...

}

Exercise

Modify the fragment shader so that the color of the triangle is a uniform variable. Don't forget to set a value for it in MyPanelOpenGL::paintGL() using shaderProgram->setUniformValue
Animation using QTimers
Next we will animate our triangle by adjusting the displacement value over time. For this will will need a few tools
  1. Make the displacement a member variable int he MyPanelOpenGL. By changing this variable, we can change the shift of the triangle
  2. Add a QTimer object that will periodically trigger refresh events
  3. Handle refresh events with a step() slot that updates the displacement and repaints the canvas.
To start, add the following member variables to mypanelopengl.h

 ... 

 private:

    unsigned int numVertices;
    QVector4D *vertices;
    /* add the three vars below */
    QVector4D displacement;
    QTimer* timer;
    float phase;
 
  ...

Also add a public slot to the same file
  public slots:
    /* called everytime timer fires */
    void step();

Next, hop over to the implementation in mypanelopengl.cpp

It's probably a good idea to initialize our new variables in the constructor.

MyPanelOpenGL::MyPanelOpenGL(QWidget *parent) :
    QOpenGLWidget(parent) {
  
   ...

  timer = NULL;
  phase = 0;
  displacement = QVector4D(0,0.2,0,0);
  
  ...
}
We can wait to dynamically allocate the timer until MyPanelOpenGL::initializeGL()
void MyPanelOpenGL::initializeGL()
{
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    createShaders();
    createVBOs();
    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(step()));
    timer->start(30); /* trigger every 30ms */ 
}
Here we make a new QTimer object a child widget of the parent MyPanelOpenGL object using the this keyword. We connect the timeout() signal to the step() slot and tell the timer to generate timeout events every 30 milliseconds.

Since we dynamically allocated the timer, we should probably free its memory in the destructor.

MyPanelOpenGL::~MyPanelOpenGL(){
    destroyVBOs();
    destroyShaders();
    if(timer) {
        delete timer; timer=NULL;
    }
}

Implement the step() slot as follows

void MyPanelOpenGL::step()
{
    phase += 0.01;
    displacement.setY(phase);
    update(); /* call paint */
}

Since we are modifying the displacement variable, remember to change paintGL() to use the value of the variable and not a fixed QVector4D

void MyPanelOpenGL::paintGL(){

    ...

    shaderProgram->enableAttributeArray("vPosition");
    shaderProgram->setAttributeBuffer("vPosition", GL_FLOAT, 0, 4, 0);
    /* use the displacement variable */
    shaderProgram->setUniformValue("disp",displacement);
    glDrawArrays(GL_TRIANGLES, 0, numVertices);

    ...

}
If everything is working, your triangle should gradually move off the top of the screen.
Bouncing

If we want to have the triangle smoothly bounce up and down while staying on the screen, we can modulate the displacement with a sine wave.

void MyPanelOpenGL::step()
{
    phase += 0.1;
    if(phase > 2*M_PI){
        phase -= 2*M_PI;
    }
    displacement.setY(0.2*sin(phase));
    update(); /* call paint */
}

Exercise

Wednesday
Let's start by grabbing a copy of the solution from Monday. I have placed the solution in a w02-opengl folder of the examples repo, so merge conflicts should be minimal
[~]$ cd ~/cs40/examples
[examples]$ git fetch upstream
[examples]$ git merge upstream/master
[examples]$ git push
You can either edit in the w02-opengl folder or the w01-intro/qtogl folder if your solution to the inclass exercises on Monday is working. Remember to run make in your build directory if you want to compile the code in w02-opengl. Note that the executable is called ./qtogl2 in the w02-opengl folder.

Exercise

So far, our scene only has one triangle. Using what you know, how could you make two triangles appear on the screen. Try to come up with as many ways as possible
glDrawArrays
The call glDrawArrays can draw more than just triangles. The following Lighthouse3D tutorial shows all the options for glDrawArrays. Explore GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, and GL_TRIANGLE_FAN.
Adding a color VBO
. In this exercise we will give each vertex a color using a separate VBO. Since the vertex shader deals with VBOs, we will need to assign an input variable to our vertex shader to grab this attribute. However, it is the fragment shader's job to assign the final color, so we will output the vertex color from the vertex shader to an input in the fragment shader. The fragment shader can use this additional input to set the final color.

Begin by creating a populating a new VBO. In the mypanelopengl.h file, add the following member variables.

     QVector4D *vertices;
     QVector3D *colors;  /* a new color array on the CPU */
     ...
     QOpenGLBuffer *vboVertices;
     QOpenGLBuffer *vboColors;  /* the corresponding GPU VBO */

Edit mypanelopengl.cpp.

Create the colors array on the CPU

MyPanelOpenGL::MyPanelOpenGL(QWidget *parent) :
    QOpenGLWidget(parent) {
   ...
   vboColors = NULL;
   ...
   colors = new QVector3D[numVertices];
   colors[0] = QVector3D(1,0,0);
   colors[1] = QVector3D(0,1,0);
   colors[2] = QVector3D(0,0,1);
   ...
}

Make the VBO for colors

void MyPanelOpenGL::createVBOs(){

    ... /*vboVertices init up here */

    vboColors = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    vboColors->create();
    vboColors->bind();
    vboColors->setUsagePattern(QOpenGLBuffer::StaticDraw);
    vboColors->allocate(colors, numVertices*sizeof(QVector3D));

    delete [] colors; colors=NULL;
    
    ...
}

Remember to clean up

void MyPanelOpenGL::destroyVBOs(){
    ...
    if (vboColors){
        vboColors->release();
        delete vboColors; vboColors=NULL;
    }
}

Tell the VAO about the new VBO and its properties

void MyPanelOpenGL::setupVAO(){
    vao->bind();
    shaderProgram->bind();
    vboVertices->bind();
    shaderProgram->enableAttributeArray("vPosition");
    shaderProgram->setAttributeBuffer("vPosition", GL_FLOAT, 0, 4, 0);
    /*BEGIN*/
    vboColors->bind();
    shaderProgram->enableAttributeArray("vColor");
    shaderProgram->setAttributeBuffer("vColor", GL_FLOAT, 0, 3, 0);
    /*END*/
    shaderProgram->release();
    vao->release();

}

That's all for MyPanelOpenGL, but note that we are connecting the colors VBO to a variable vColor. We need to add this feature in the shaders.

#version 410

in vec4 vPosition;
in vec3 vColor;  /* from VBO */
uniform vec4 displacement;

out vec3 color;  /* to fragment shader */

void
main()
{   
    gl_Position =  vPosition + displacement;
    color = vColor; /* pass through */
}

#version 410

in vec3 color;      /* from Vertex shader */
out vec4 fragColor;

void
main()
{
    fragColor.rgb = color;  /* set up the swizzle */
    fragColor.a = 1.;       /* don't touch the dizzle */
}
Note how the colors smoothly change. What happens if you add the qualifier "flat" in front of the color variable in the vertex and fragment shaders?
Geometry intro
Before we dive into drawing, manipulating, and lighting shapes, we need to know a little bit more about how to represent geometric objects in OpenGL.