Managing Objects in OpenGL Smartly
18 May 2020A 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.
-
An enum is declared for each supported data type:
@enum(OPENGL_DATA_TYPE, ODT_NONE, ODT_INT, ODT_FLOAT, ODT_VEC2F, ODT_VEC3F, ODT_VEC4F, ODT_MAT2F, ODT_MAT3F, ODT_MAT4F)
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
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