I am writing a program using Qt5 and OpenGL. It is an interactive 3D environment that allows the user to, at any point, import a number of triangulated meshes (among other things). I was initially using the fixed function pipeline, but I learned about shaders and would like to convert to them.
After I converted my code to use shaders, I ran into a strange issue that I can't figure out, which the code example here reproduces as minimally as I could make it:
#include <QApplication>
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QTimer>
#include <QMatrix4x4>
#include <QVector3D>
#include <QVector>
#include <iostream>
#define TEMP_ERR_CHK { GLenum err = glGetError(); std::cout << __FILE__ << " " << __LINE__ << ": OpenGL error code: " << (err) << std::endl; }
class Mesh
{
public:
Mesh(QOpenGLFunctions_3_3_Core* context_in)
{
context = context_in;
init_vbos();
}
void init_vbos()
{
std::cout << "I AM INITIALIZING THE MESH VBOS" << std::endl;
vertices = {0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f};
colors = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
normals = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f};
// Create VAO and VBOs
context->glGenVertexArrays(1, &vao);
context->glBindVertexArray(vao);
// Position VBO
context->glGenBuffers(1, &vboPositions);
context->glBindBuffer(GL_ARRAY_BUFFER, vboPositions);
context->glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(GLfloat), vertices.data(), GL_STATIC_DRAW);
context->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
context->glEnableVertexAttribArray(0);
// Color VBO
context->glGenBuffers(1, &vboColors);
context->glBindBuffer(GL_ARRAY_BUFFER, vboColors);
context->glBufferData(GL_ARRAY_BUFFER, colors.size() * sizeof(GLfloat), colors.data(), GL_STATIC_DRAW);
context->glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
context->glEnableVertexAttribArray(1);
// Normal VBO
context->glGenBuffers(1, &vboNormals);
context->glBindBuffer(GL_ARRAY_BUFFER, vboNormals);
context->glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(GLfloat), normals.data(), GL_STATIC_DRAW);
context->glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
context->glEnableVertexAttribArray(2);
context->glBindVertexArray(0);
}
void draw()
{
std::cout << "drawing" << std::endl;
TEMP_ERR_CHK
context->glBindVertexArray(vao);
TEMP_ERR_CHK
context->glDrawArrays(GL_TRIANGLES, 0, 36);
context->glBindVertexArray(0);
}
private:
GLuint vao, vboPositions, vboColors, vboNormals;
QVector<GLfloat> vertices, colors, normals;
float rotationAngle;
QOpenGLFunctions_3_3_Core* context;
};
class OpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
public:
OpenGLWidget(QWidget* parent = nullptr)
: QOpenGLWidget(parent), program(nullptr), rotationAngle(24.0f) {}
virtual void mousePressEvent(QMouseEvent* event) override final
{
std::cout << "AAAA" << std::endl;
mesh_to_draw = new Mesh(this); //LOCATION 0
this->update();
}
~OpenGLWidget() {
}
protected:
void initializeGL() override {
initializeOpenGLFunctions();
std::cout << "I AM INITIALIZING THE OPENGL" << std::endl;
glEnable(GL_DEPTH_TEST);
// Shader program initialization
program = new QOpenGLShaderProgram(this);
program->addShaderFromSourceCode(QOpenGLShader::Vertex,
R"(
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec3 normal;
out vec3 fragColor;
out vec3 fragNormal;
out vec3 fragPosition;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
fragColor = color;
fragNormal = normalize(mat3(transpose(inverse(model))) * normal);
fragPosition = vec3(model * vec4(position, 1.0f));
gl_Position = projection * view * vec4(fragPosition, 1.0f);
})");
program->addShaderFromSourceCode(QOpenGLShader::Fragment,
R"(
#version 330 core
in vec3 fragColor;
in vec3 fragNormal;
in vec3 fragPosition;
out vec4 fragColorOut;
uniform vec3 lightPosition;
uniform vec3 viewPosition;
void main() {
// Simple Phong lighting model
vec3 ambient = 0.1 * fragColor;
vec3 norm = normalize(fragNormal);
vec3 lightDir = normalize(lightPosition - fragPosition);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * fragColor;
vec3 viewDir = normalize(viewPosition - fragPosition);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = 0.5 * spec * vec3(1.0);
fragColorOut = vec4(ambient + diffuse + specular, 1.0);
})");
program->link();
// mesh_to_draw = new Mesh(this); //LOCATION 1
}
void paintGL() override {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
program->bind();
// Set up transformation matrices
QMatrix4x4 model;
model.rotate(rotationAngle, 0.0f, 1.0f, 0.0f); // Rotate around Y axis
QMatrix4x4 view;
view.lookAt(QVector3D(0.0f, 0.0f, 3.0f), QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0f, 1.0f, 0.0f));
QMatrix4x4 projection;
projection.perspective(45.0f, width() / float(height()), 0.1f, 100.0f);
program->setUniformValue("model", model);
program->setUniformValue("view", view);
program->setUniformValue("projection", projection);
program->setUniformValue("lightPosition", QVector3D(1.0f, 1.0f, 1.0f));
program->setUniformValue("viewPosition", QVector3D(0.0f, 0.0f, 3.0f));
if (mesh_to_draw) mesh_to_draw->draw();
program->release();
}
void resizeGL(int w, int h) override {
glViewport(0, 0, w, h);
}
private:
QOpenGLShaderProgram* program;
Mesh* mesh_to_draw = nullptr;
float rotationAngle;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
OpenGLWidget widget;
widget.setWindowTitle("Rotating Cube");
widget.resize(800, 600);
widget.show();
return app.exec();
}
The above code contains a mesh class that holds vertex, normal, and color data, as well as the VBOs and VAO that I am using to render it. There is a function in there that initializes these objects.
There is an OpenGL widget class that handles rendering, which inherits from QOpenGLWidget and QOpenGLFunctions_3_3_Core. The code contains two commented locations: Location 0 and location 1. These represent different locations that initialize the mesh's various resources. Initialization at location 1 gives me the expected result. I see a triangle on the screen and all is well. The problem is that I have to initialize the mesh inside the initializeGL function, which is called at the start of the program. I want to be able to add a mesh at some arbitrary point during runtime. To mimic this, I added a mousePressEvent that initializes the mesh (location 0). However, when I initialize this way, I see nothing on the screen, and OpenGL gives me the error 1282 (invalid operation). It seems like by initializing the VAO after the first draw call (or something), the VAO is no longer valid.
For completeness, here are the terminal outputs for each case: Location 1:
I AM INITIALIZING THE OPENGL
I AM INITIALIZING THE MESH VBOS
drawing
src/main.cpp 64: OpenGL error code: 0
src/main.cpp 66: OpenGL error code: 0
drawing
src/main.cpp 64: OpenGL error code: 0
src/main.cpp 66: OpenGL error code: 0
(triangle displayed properly)
Location 0:
I AM INITIALIZING THE OPENGL
AAAA
I AM INITIALIZING THE MESH VBOS
drawing
src/main.cpp 64: OpenGL error code: 0
src/main.cpp 66: OpenGL error code: 1282
(nothing on screen)
How can I go about fixing this?
From the Qt5 documentation, emphasis mine:
Your widget's OpenGL rendering context is made current when paintGL(), resizeGL(), or initializeGL() is called. If you need to call the standard OpenGL API functions from other places (e.g. in your widget's constructor or in your own paint functions), you must call makeCurrent() first.
The problem is that the function init_vbos()
is called while another OpenGL context is current, in this case one used by Qt internally. Thus the VAO generated is associated with that context, and VAOs cannot be shared across contexts. I would presume that it's the function glBindVertexArray() that produces the GL_INVALID_OPERATION
(1282).
The solution is to call makeCurrent()
before making OpenGL calls outside of the three functions mentioned. This guarantees that the corresponding context is current. Since makeCurrent()
is part of QOpenGLWidget
, the simplest method is to call it in mousePressEvent(..)
, before instantiating the Mesh.
Alternatively, get the QOpenGLContext
from the QOpenGLWidget
, pass it to init_vbos()
and call makeCurrent()
on it there.
Note that it's not necessary to call makeCurrent()
in the Mesh::draw()
function. It is only called from OpenGLWidget::paintGL()
, and thus Qt ensures that the appropriate context is current.
EDIT: Thanks to G.M. for pointing out that makeCurrent()
is not part of context
. The variable context
does not hold an OpenGL context, but the set of OpenGL function pointers.