andersch.dev

<2022-05-04 Wed>

OpenGL

OpenGL (Open Graphics Library) is an open specification for a 2D/3D rendering API. Cross-platform implementations are provided through graphics drivers - usually by the GPU manufacturers. It uses glsl as its shading language.

Vertex Array Object

  • A vertex array object (VAO) can be bound just like a vertex buffer object (VBO) and any subsequent vertex attribute calls from that point on will be stored inside the VAO.
  • Whenever we want to draw the object, we just bind the corresponding VAO.
  • Core OpenGL requires that we use a VAO.
  • A vertex array object stores the following:

    • Calls to glEnableVertexAttribArray or glDisableVertexAttribArray.
    • Vertex attribute configurations via glVertexAttribPointer.
    • Vertex buffer objects associated with vertex attributes by calls to glVertexAttribPointer.
    u32 vao;
    glGenVertexArrays(1, &vao); // nr of vao's to generate & where to store them
    if (vao == GL_INVALID_VALUE)
        error();
    

Debugging OpenGL

OpenGL versions above 4.3 (or drivers who have implemented the KHR_debug extension) allow access to a debug context. To activate and enable it (in SDL/GLFW):

/* prototype for callback */
void GLAPIENTRY my_callback(GLenum source, GLenum type, GLuint id,
                            GLenum severity, GLsizei length,
                            const GLchar* message, const void* userParam);

/* Set config flag with SDL/GLFW/... */
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT,1);

/* add debug callback */
glEnable(GL_DEBUG_OUTPUT); // or GL_DEBUG_OUTPUT_SYNCHRONOUS to avoid race conditions
glDebugMessageCallback(my_callback, userdata);

More extensive debugging capabilities can be achieved by using a graphics debugger such as RenderDoc.

A Better Way of Writing OpenGL Code

To better manage the state-machine of OpenGL, we can define macros that open up scopes that push/pop specific states. Note that this might come with performance costs, as some of these macros need to query the current state of the OpenGL context from the GPU.

/* helper macros */
#define TOKEN_PASTE(a, b) a##b
#define CONCAT(a,b) TOKEN_PASTE(a,b)
#define UQ(name) CONCAT(name, __LINE__) /* unique identifier */

#define scope_begin_end_var(begin, end, var) \
    for (int UQ(var) = (begin, 0); (UQ(var) == 0); (UQ(var) += 1), end)


/* NOTE: these do not restore the previous state */
#define scope_gl_use_program(id) \
    scope_begin_end_var(glUseProgram(id), glUseProgram(0), prog)

#define scope_gl_bind_array_buffer(vbo) \
    scope_begin_end_var(glBindBuffer(GL_ARRAY_BUFFER, vbo), glBindBuffer(GL_ARRAY_BUFFER, 0), arrbuf)

/* TODO probably doesn't compile because of the comma (?) */
#define scope_gl_bind_ssbo(ssbo, binding) \
    scope_begin_end_var((glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo), glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo)), (glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0), glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)), ssbo)

#define scope_gl_bind_vertex_array(vao) \
    scope_begin_end_var(glBindVertexArray(vao), glBindVertexArray(vao), vertarr)

#define scope_gl_bind_texture2d(tex_id) \
    scope_begin_end_var(glBindTexture(GL_TEXTURE_2D, tex_id), glBindTexture(GL_TEXTURE_2D), texbind)

/* GL_DEPTH_TEST, GL_BLEND, ... */
#define scope_gl_enable(enumval) \
    scope_begin_end_var(glEnable(enumval), glDisable(enumval), glenable)


/* NOTE: these do restore previous state (by querying, which could be slow) */
/* following ones might be less needed...*/
#define scope_gl_viewport(x,y,w,h) \
    for (GLint UQ(view)[4], UQ(i) = (glGetIntegerv(GL_VIEWPORT, UQ(view)), glViewport(x, y, w, h), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glViewport(UQ(view)[0], UQ(view)[1], UQ(view)[2], UQ(view)[3])))
#define scope_gl_clearcolor(r,g,b,a) \
    for (GLfloat UQ(clear)[4], UQ(i) = (glGetFloatv(GL_COLOR_CLEAR_VALUE, UQ(clear)), glClearColor(r,g,b,a), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glClearColor(UQ(clear)[0], UQ(clear)[1], UQ(clear)[2], UQ(clear)[3])))

#define scope_gl_blend_func(src,dst) \
    for (GLint UQ(s), UQ(d), UQ(i) = (glGetIntegerv(GL_BLEND_SRC, &UQ(s)), glGetIntegerv(GL_BLEND_DST, &UQ(s), glBlendFunc(src, dst), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glBlendFunc(UQ(s), UQ(d))))

#define scope_gl_blend_eq(eq) \
    for (GLint UQ(e), UQ(i) = (glGetIntegerv(GL_BLEND_EQUATION, &UQ(e)), glBlendEquation(eq), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glBlendEquation(UQ(e))))

#define scope_gl_cullface_mode(mode) \
    for (GLint UQ(m), UQ(i) = (glGetIntegerv(GL_CULL_FACE_MODE, &UQ(m)), glCullFace(mode), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glCullFace(UQ(m))))

#define scope_gl_frontface_orient(orient) \
    for (GLint UQ(fo), UQ(i) = (glGetIntegerv(GL_FRONT_FACE, &UQ(fo)), glFrontFace(orient), 0); \
         (UQ(i) == 0); (UQ(i) += 1, glFrontFace(UQ(fo))))

/*
  More possible push/pop functions include
     glEnableVertexAttribArray/glDisableVertexAttribArray
     glBindFramebuffer
     glTexParameter : gl_texture2d_param(param,val)
     glActiveTexture() : glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTextureUnit);

     look at cf push/pop drawing functions for more

     one could also add calls to glError() at the end of every scope

     gl_upload_uniform that wraps glUseProgram, glGetUniformLocation and glUniform{1iv,Matrix4fv,..}
     maybe gl_upload_uniform(prog,name,type) glUseProgram(prog); glUniform##type(glGet(prog,"name"), ...);
*/
int main() {

    // ...

    //scope_gl_use_program(prog_id)
    // scope_gl_bind_array_buffer(vbo)
    //  scope_gl_bind_vertex_array(vao)
    //{
    //    // ...
    //    glDrawArrays(GL_TRIANGLES, 0, vertex_count);
    //}
}

Vertex Format Pulling vs Pushing

The conventional way of specifying vertex formats is to "push" the data as input to the shader using VAO with glVertexAttribPointer. This way, we define format and stride and the GPU executes the vertex shader vertex by vertex.

Vertex format pulling on the other hand involves uploading the vertex buffer and then using a vertex ID to access it:

float3 position = buffer[vertexId].pos.xyz;

Using this approach, VAOs and VBOs are no longer needed (an empty VAO might still need to be bound). The VBO containing the vertex data is replaced by an SSBO. See Programmable Vertex Pulling.

See:

Resources