Managing Objects in OpenGL Smartly

A large proportion of my OpenGL coding style comes from learnopengl, which is a wonderful website for GL beginners. Props to the unselfish author, Joey de Vries, for keeping and sharing such wonderful contents.

One of the biggest obstacles of using OpenGL is the huge amount of code needed for a simplest project: check out the program of drawing a static triangle, which has ~180 lines of code! As it turns out, trivial works in OpenGL, such as compiling shaders, linking programs, declaring VAOs, contribute to a huge amount of code. If we can somehow construct a template to simplify these highly similar procedures, we can greatly speed up the development efficiency of (simple) OpenGL programs.

Because I am trying to explore Julia’s potential to write graphics programs, the examples in this article are written in Julia. It is worth noticing that these design can be extended to other languages as well, especially those that are object-oriented.

GL Programs Are First-class citizen

Common Definitions

Most code samples are excerpted from utility.jl, if there is any undocumented symbols or changes, please refer to the source file.

Smart Shaders/Programs/Uniform

This part is pretty straightforward. An OpenGL program can only have three types of shaders (i.e. vertex, geometry, fragment). We can create each shader with glCreateShader and compile them with glShaderSource and glCompileShader. Later on, we can link shaders into one program with glLinkProgram.

In Julia, the compilation of shaders and linkage of programs can be summarized into the following two functions:

function _compile_opengl_shader(gl_shader_enum::GLenum, shader::String)
    shader_id = glCreateShader(gl_shader_enum)
    source_ptrs = Ptr{GLchar}[pointer(shader)]

    gl_int_param = GLint[0]
    gl_int_param[1] = length(shader) # specify source length

    glShaderSource(shader_id, 1, pointer(source_ptrs), pointer(gl_int_param))
    glCompileShader(shader_id)

    message = Array{UInt8, 1}()
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, pointer(gl_int_param))
    status = gl_int_param[1]

    if status == GL_FALSE
        resize!(message, _INFOLOG_SIZE)
        gl_sizei_param = GLsizei[0]
        glGetShaderInfoLog(shader_id, _INFOLOG_SIZE, pointer(gl_sizei_param), pointer(message))
    end

    status, shader_id, String(message)
end

function _link_opengl_program(gl_shader_ids::Array{GLuint, 1})
    # link programs
    gl_program_id = glCreateProgram()
    for gl_shader_id in gl_shader_ids
        glAttachShader(gl_program_id, gl_shader_id)
    end

    gl_int_param = GLint[0]

    glLinkProgram(gl_program_id)
    glGetProgramiv(gl_program_id, GL_LINK_STATUS, pointer(gl_int_param))
    status = gl_int_param[1]

    message = Array{UInt8, 1}()

    if status == GL_FALSE
        resize!(message, _INFOLOG_SIZE)
        gl_sizei_param = GLsizei[0]
        glGetProgramInfoLog(gl_program_id, _INFOLOG_SIZE, pointer(gl_sizei_param), pointer(message))
    end

    status, gl_program_id, String(message)
end

The key to this step is to acquire the program id after linkage, given shader sources as input. In utility.jl, the _compile_link_opengl_program function encapsulates the two function above into a “black-box” function that completes this task with error checking.

Smart Buffers

utility.jl source

Download

Click to expand
module GraphicsUtil
using ModernGL
using Base
using Printf

export @debug_msg, prn_stderr, @exported_enum
export create_opengl_program, opengl_program_add_uniform!, update_uniform
export GLUniform, GLProgram
export OPENGL_DATA_TYPE

const _debug_message = true
const _INFOLOG_SIZE = 1500

macro exported_enum(name, args...)
    esc(quote
        @enum($name, $(args...))
        export $name
        $([:(export $arg) for arg in args]...)
        end)
end

@exported_enum(OPENGL_DATA_TYPE, ODT_NONE,
ODT_FLOAT, ODT_INT,
ODT_VEC2F, ODT_VEC3F, ODT_VEC4F,
ODT_MAT2F, ODT_MAT3F, ODT_MAT4F)


const _uniform_vec_f_functions = [glUniform2fv, glUniform3fv, glUniform4fv]
const _uniform_mat_f_functions = [glUniformMatrix2fv, glUniformMatrix3fv, glUniformMatrix4fv]

function prn_stderr(obj...)
    println(Base.stderr, obj...)
end

macro debug_msg(obj...)
    if _debug_message
        escaped_objs = [esc(o) for o in obj]
        return :(prn_stderr($(escaped_objs...)))
    else
        return :(nothing)
    end
end

struct GLUniform
    type::OPENGL_DATA_TYPE
end

mutable struct GLProgram
    program_id::GLuint
    uniforms::Dict{String, GLUniform}
end

GLUniform() = GLUniform(ODT_NONE)
GLProgram() = GLProgram(0, Dict{String, GLUniform}())

function _compile_opengl_shader(gl_shader_enum::GLenum, shader::String)
    shader_id = glCreateShader(gl_shader_enum)
    source_ptrs = Ptr{GLchar}[pointer(shader)]

    gl_int_param = GLint[0]
    gl_int_param[1] = length(shader) # specify source length

    glShaderSource(shader_id, 1, pointer(source_ptrs), pointer(gl_int_param))
    glCompileShader(shader_id)

    message = Array{UInt8, 1}()
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, pointer(gl_int_param))
    status = gl_int_param[1]

    if status == GL_FALSE
        resize!(message, _INFOLOG_SIZE)
        gl_sizei_param = GLsizei[0]
        glGetShaderInfoLog(shader_id, _INFOLOG_SIZE, pointer(gl_sizei_param), pointer(message))
    end

    status, shader_id, String(message)
end

function _link_opengl_program(gl_shader_ids::Array{GLuint, 1})
    # link programs
    gl_program_id = glCreateProgram()
    for gl_shader_id in gl_shader_ids
        glAttachShader(gl_program_id, gl_shader_id)
    end

    gl_int_param = GLint[0]

    glLinkProgram(gl_program_id)
    glGetProgramiv(gl_program_id, GL_LINK_STATUS, pointer(gl_int_param))
    status = gl_int_param[1]

    message = Array{UInt8, 1}()

    if status == GL_FALSE
        resize!(message, _INFOLOG_SIZE)
        gl_sizei_param = GLsizei[0]
        glGetProgramInfoLog(gl_program_id, _INFOLOG_SIZE, pointer(gl_sizei_param), pointer(message))
    end

    status, gl_program_id, String(message)
end

function _compile_link_opengl_program(shaders::Dict{String, String})
    has_vertex_shader::Bool = false
    has_geometry_shader::Bool = false
    has_fragment_shader::Bool = false

    shader_keys = ["vertex", "geometry", "fragment"]
    shader_enums = [GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER]
    shader_flags = zeros(Bool, length(shader_keys))

    gl_shader_ids = Array{GLuint, 1}()

    # compile individual shaders
    for i = 1:length(shader_keys)
        if haskey(shaders, shader_keys[i])
            shader_flags[i] = true
            status, shader_id, message = _compile_opengl_shader(shader_enums[i], shaders[shader_keys[i]])
            digest = @sprintf("%s shader compilation failed", shader_keys[i])
            if status == GL_FALSE
                prn_stderr(digest, "\n")
                prn_stderr(message)
                throw(ErrorException(digest))
            else
                msg = @sprintf("successfully compiled opengl %s shader %d", shader_keys[i], shader_id)
                @debug_msg(msg)
                push!(gl_shader_ids, shader_id)
            end
        end
    end

    if shader_flags[1] == false
        throw(ErrorException("no vertex shader"))
    elseif shader_keys[3] == false
        throw(ErrorException("no fragment shader"))
    end

    status, gl_program_id, message = _link_opengl_program(gl_shader_ids)
    if status == GL_FALSE
        digest = "GL program linkage failed"
        prn_stderr(digest, "\n")
        prn_stderr(message)
        throw(ErrorException(digest))
    end

    @debug_msg("successfully linked opengl program ", gl_program_id)

    # if we reach here, the linkage is sucessful
    # therefore, delete individual shaders
    for gl_shader_id in gl_shader_ids
        glDeleteShader(gl_shader_id)
    end

    gl_program_id
end


function opengl_program_add_uniform!(program::GLProgram, name::String, type::OPENGL_DATA_TYPE)
    uniform = GLUniform(type)
    if haskey(program.uniforms, name)
        prn_stderr(@sprintf("warning: uniform \'%s\' already existed in program %d\n", name, program.program_id))
    end
    program.uniforms[name] = uniform
    @debug_msg(@sprintf("opengl program %d added uniform (name=\'%s\', type=\'%s\')", program.program_id,
                        name, type))

end

function create_opengl_program(shaders::Dict{String, String}, uniforms::Dict{String, OPENGL_DATA_TYPE})
    program_id = _compile_link_opengl_program(shaders)
    program = GLProgram()
    program.program_id = program_id
    for (name, type) in uniforms
        opengl_program_add_uniform!(program, name, type)
    end
    program
end

function _check_uniform_data_type(data, type::DataType)
    if !isa(data, type)
        digest = @sprintf("uniform data type mismatch: expected \'%s\', get \'%s\'", type, typeof(data))
        throw(ErrorException(digest))
    end
end

function _check_uniform_data_shape(data, shape)
    if size(data) != shape
        digest = @sprintf("uniform data size mismatch: expected \'%s\', get \'%s\'", shape, size(data))
        throw(ErrorException(digest))
    end
end


function update_uniform(program::GLProgram, name::String, data)
    # retrieve the uniform
    if !haskey(program.uniforms, name)
        throw(ErrorException(@sprintf("undefined uniform name \'%s\'", name)))
    end
    uniform = program.uniforms[name]

    if uniform.type == ODT_NONE
        throw(ErrorException("invalid uniform type: ODT_NONE (uniform may not be properly initialized)"))
    end

    loc = glGetUniformLocation(program.program_id, name)

    if uniform.type == ODT_FLOAT
        _check_uniform_data_type(data, Float32)
        val_f::Float32 = data
        glUniform1f(loc, val_f)
    elseif uniform.type == ODT_INT
        _check_uniform_data_type(data, Integer)
        val_i::GLint = data
        glUniform1i(loc, val_i)
    elseif uniform.type == ODT_VEC2F || uniform.type == ODT_VEC3F || uniform.type == ODT_VEC4F
        vec_offset = Integer(uniform.type) - Integer(ODT_VEC2F)
        vec_dim = vec_offset + 2
        _check_uniform_data_type(data, Array{Float32, 1})
        _check_uniform_data_shape(data, (vec_dim,))
        _uniform_vec_f_functions[1 + vec_offset](loc, 1, pointer(data))

    elseif uniform.type == ODT_MAT2F || uniform.type == ODT_MAT3F || uniform.type == ODT_MAT4F
        mat_offset = Integer(uniform.type) - Integer(ODT_MAT2F)
        mat_dim = mat_offset + 2
        _check_uniform_data_type(data, Array{Float32, 2})
        _check_uniform_data_shape(data, (mat_dim, mat_dim))
        _uniform_mat_f_functions[1 + mat_offset](loc, 1, GL_FALSE, pointer(data))
    else
        throw(ErrorException("invalid uniform type: ", uniform.type))
    end
end


end