Hello, and welcome to the pages of Greg’s Journal!

In this post, I would like to discuss the abstraction of shaders in my code.

“The OpenGL rendering pipeline is initiated when you perform a rendering operation. Rendering operations require the presence of a properly-defined vertex array object and a linked Program Object or Program Pipeline Object, which provides the shaders for the programmable pipeline stages.” [1]

If you would strip all of the “objects” from this explanation, what they are trying to say, is that the OpenGL pipeline performs several transformations of graphics data before the colorful pixels hit the screen. And some of those transformations are programmable (so you can program, while programming).

What we will discuss

Programmable stages

There are two main stages, which are of interest for me – vertex shader and fragment shader.

OpenGl pipeline

Those are represented by corresponding types:

common_gl/com.blaster.gl.GlProgram::GlShaderType

enum class GlShaderType(val type: Int) {
    VERTEX_SHADER(backend.GL_VERTEX_SHADER),
    FRAGMENT_SHADER(backend.GL_FRAGMENT_SHADER)
}

The shader itself is a relatively straightforward class, which compiles GLSL code and checks for errors:

common_gl/com.blaster.gl.GlProgram::GlShader

class GlShader(val type: GlShaderType, source: String) {
    val handle = glCheck { backend.glCreateShader(type.type) }

    init {
        glCheck {
            backend.glShaderSource(handle, source)
            backend.glCompileShader(handle)
        }
        val isCompiled = backend.glGetShaderi(handle, backend.GL_COMPILE_STATUS)
        if (isCompiled == backend.GL_FALSE) {

I am also printing the source of the shader to ease debugging and error deciphering

            val sb = StringBuffer()
            source.lines().forEachIndexed { index, line ->
                sb.append("$index $line\n")
            }
            val reason = "Failed to compile shader:\n\n$sb\n\nWith reason:\n\n${backend.glGetShaderInfoLog(handle)}"
            throw IllegalStateException(reason)
        }
    }

    fun delete() {
        glCheck { backend.glDeleteShader(handle) }
    }
}

I wonder if it is possible to write shaders in Kotlin directly with intermediate representation [2]. It should be – I know about some projects [3] doing that [4]. It will definitely improve readability and maintenance for me. I will probably investigate into that direction at some point.

Linking programs

Two shaders (vertex and fragment) are combined into a program. GlProgram class performs linking and error checking:

common_gl/com.blaster.gl.GlProgram::createProgram

private fun createProgram() {
    check(vertexShader.type == GlShaderType.VERTEX_SHADER)
    check(fragmentShader.type == GlShaderType.FRAGMENT_SHADER)
    glCheck {
        backend.glAttachShader(handle, vertexShader.handle)
        backend.glAttachShader(handle, fragmentShader.handle)
        backend.glLinkProgram(handle)
    }
    val isLinked = backend.glGetProgrami(handle, backend.GL_LINK_STATUS)
    if (isLinked == backend.GL_FALSE) {
        throw IllegalStateException(backend.glGetProgramInfoLog(handle))
    }
    cacheUniforms()
}

One can notice that I am caching uniform locations after creation – we will talk about it shortly.

Uniforms caching

In my code, I prefer to look for uniforms as part of the program creation process. Usually, that happens once during the initialization, which is convenient. Uniform locations are then stored in the cache for a lookup speedup when we need to send data:

common_gl/com.blaster.gl.GlProgram::cacheUniforms

private fun cacheUniforms() {
    GlUniform.values().forEach {
        val location = glCheck { backend.glGetUniformLocation(handle, it.label) }
        if (location != -1) {
            uniformLocations[it] = location
        }
    }
}

I am using a predefined set of uniform names in code:

common_gl/com.blaster.gl.GlUniform

enum class GlUniform(val label: String) {
    UNIFORM_MODEL_M             ("uModelM"),
    UNIFORM_PROJ_M              ("uProjectionM"),
    UNIFORM_VIEW_M              ("uViewM"),
    UNIFORM_EYE                 ("uEye"),

    UNIFORM_TEXTURE_POSITION    ("uTexPosition"),
    UNIFORM_TEXTURE_NORMAL      ("uTexNormal"),
    UNIFORM_TEXTURE_DIFFUSE     ("uTexDiffuse"),

    UNIFORM_TEXTURE_ALBEDO      ("uTexAlbedo"),
    UNIFORM_TEXTURE_METALLIC    ("uTexMetallic"),
    UNIFORM_TEXTURE_ROUGHNESS   ("uTexRoughness"),
    UNIFORM_TEXTURE_AO          ("uTexAo"),

    UNIFORM_TEXTURE_MAT_AMB_SHINE("uTexMatAmbientShine"),
    UNIFORM_TEXTURE_MAT_DIFF_TRANSP ("uTexMatDiffTransp"),
    UNIFORM_TEXTURE_MAT_SPECULAR("uTexMatSpecular"),

    UNIFORM_LIGHTS_POINT_CNT    ("uLightsPointCnt"),
    UNIFORM_LIGHTS_DIR_CNT      ("uLightsDirCnt"),
    UNIFORM_LIGHT_VECTOR        ("uLights[%d].vector"),
    UNIFORM_LIGHT_INTENSITY     ("uLights[%d].intensity"),

    UNIFORM_MAT_AMBIENT         ("uMatAmbient"),
    UNIFORM_MAT_DIFFUSE         ("uMatDiffuse"),
    UNIFORM_MAT_SPECULAR        ("uMatSpecular"),
    UNIFORM_MAT_SHINE           ("uMatShine"),
    UNIFORM_MAT_TRANSP          ("uMatTransp"),

    UNIFORM_COLOR               ("uColor"),

    UNIFORM_CHAR_INDEX          ("uCharIndex"),
    UNIFORM_CHAR_START          ("uCharStart"),
    UNIFORM_CHAR_SCALE          ("uCharScale"),

    UNIFORM_WIDTH               ("uWidth"),
    UNIFORM_HEIGHT              ("uHeight"),

    UNIFORM_SCALE_FLAG          ("uScaleFlag"),
    UNIFORM_TRANSPARENCY_FLAG   ("uTransparencyFlag");
}

Uniforms sending

GlProgram also has a set of methods to send different type of data into the shader, for example:

common_gl/com.blaster.gl.GlProgram::setUniform

fun setUniform(uniform: GlUniform, value: Matrix4f) {
    value.get(bufferMat4)
    glCheck { backend.glUniformMatrix4fv(uniformLocations[uniform]!!, 1, false, bufferMat4) }
}

Updating the texture unit works basically in the same way:

common_gl/com.blaster.gl.GlProgram::setTexture

fun setTexture(uniform: GlUniform, texture: GlTexture) {
    setUniform(uniform, texture.unit)
}

And here is an example of usage:

common_gl/com.blaster.techniques.SkyboxTechnique::skybox

fun skybox(camera: Camera) {
    onlyRotationM.set(camera.calculateViewM())
    noTranslationM.set(onlyRotationM)
    glBind(listOf(program, cube, diffuse)) {
        program.setUniform(GlUniform.UNIFORM_PROJ_M, camera.projectionM)
        program.setUniform(GlUniform.UNIFORM_VIEW_M, noTranslationM)
        program.setTexture(GlUniform.UNIFORM_TEXTURE_DIFFUSE, diffuse)
        cube.draw()
    }
}

Reading and loading programs

I am storing my shaders as a part of my resources pack. To access and load them, I created a small class, which can read the program code directly from assets:

common_gl/com.blaster.assets.ShadersLib

class ShadersLib(private val assetStream: AssetStream) {
    fun loadProgram(vertShaderAsset: String, fragShaderAsset: String) : GlProgram = GlProgram(
            GlShader(GlShaderType.VERTEX_SHADER, slurpAsset(vertShaderAsset)),
            GlShader(GlShaderType.FRAGMENT_SHADER, slurpAsset(fragShaderAsset)))

    private fun slurpAsset(filename: String): String {
        val stringBuilder = StringBuilder()
        val inputStream = assetStream.openAsset(filename)
        val bufferedReader = BufferedReader(InputStreamReader(inputStream, Charset.defaultCharset()))
        bufferedReader.use {
            var line = bufferedReader.readLine()
            while (line != null) {
                stringBuilder.append("$line\n")
                line = bufferedReader.readLine()
            }
        }
        return stringBuilder.toString()
    }
}

This class will retrieve the code and compile it into the program:

common_gl/com.blaster.techniques.SimpleTechnique::create

Compiling and storing shader program

fun create(shadersLib: ShadersLib) {
    program = shadersLib.loadProgram("shaders/simple/no_lighting.vert", "shaders/simple/no_lighting.frag")
}

That is it for now – hope you liked it. I will see you again soon!

References


Leave a Reply

Your email address will not be published. Required fields are marked *