Hello, and welcome to the pages of my weekly journal!

Today I would like to talk about debugging.

There are multiple ways of how you can approach the debugging of visual applications. Some of those ways are more convenient than the conventional approach with the debugger. In this article, I will highlight some of the tools I am using to convey information visually. We will be talking about presenting text on the screen and, also, briefly, I will touch on OpenGL Immediate Mode [1].

Today we will discuss

Presenting text

Let us start with the most ubiquitous way of presenting information – the text.

There are roughly two ways of how characters on the screen can be rendered.

The first one is a “retro” approach – all characters are pre-rendered to a texture, and then we select glyphs from this texture with texture coordinates.

The second approach allows for customizations. With the help of third-party code [2] we can render each character with parameters and of variable size.

Since the first approach is quick, dirty, and also look dated – it is obviously my favorite 🙂

Here is the font I am using in Blaster:

Retro Font

It reminds me of simpler times in programming when you were able to control your IDE, not the other way around.

We will be discussing the code top to bottom – this way, I can highlight the high-level ideas before jumping into the details of the implementation.

Console

Just rendering the text is not enough. Usually, we want classes which hide all the complexities behind the easy to use interfaces. In my case, this is achieved with the Console class.

The Console is a helper class, which allows printing the debugging information in a line-by-line fashion.

We need to add the class to the app, and then we can send the logs to be printed:

common_gl/com.blaster.toolbox.Console::line

private fun line(text: String, level: Level) {
    lines.add(Line(text, System.currentTimeMillis(), level))
}

As in most of the logging frameworks, each line has its level of priority:

common_gl/com.blaster.toolbox.Console::Level

enum class Level { FAILURE, INFO, SUCCESS }

Each level is reflected by its color in text.

Since console also tracks the time to live for each line, we need to throttle it:

common_gl/com.blaster.toolbox.Console::tick

fun tick() {
    val current = System.currentTimeMillis()
    val iterator = lines.iterator()
    while (iterator.hasNext()) {
        val line = iterator.next()
        if (current - line.timestamp > timeout) {
            iterator.remove()
        }
    }
}

To print the current buffer of the console, we can use the following:

common_gl/com.blaster.toolbox.Console::render

fun render(callback: (position: Vector2f, text: String, color: Vector3f, scale: Float) -> Unit) {
    lines.forEachIndexed { index, line ->
        val color = when (line.level) {
            Level.FAILURE -> COLOR_FAILURE
            Level.INFO -> COLOR_INFO
            Level.SUCCESS -> COLOR_SUCCESS
        }
        POSITION.set(START_X, START_Y - TEXT_SCALE * index * 2f)
        callback.invoke(POSITION, line.text, color, TEXT_SCALE)
    }
}

Here is the example:

desktop/com.blaster.impl.Text::onTick

override fun onTick() {
    GlState.clear()
    console.tick()
    technique.draw {
        console.render { position, text, color, scale ->
            technique.text(text, position, scale, color)
        }
    }
}

Text technique

To render the text, I am using TextTechnique.

First, we need to create a shader, a font texture and a rect:

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

fun create(shadersLib: ShadersLib, textureLib: TexturesLib, font: String = "textures/font.png") {
    program = shadersLib.loadProgram("shaders/text/text.vert", "shaders/text/text.frag")
    diffuse = textureLib.loadTexture(font)
    rect = GlMesh.rect()
}

Draw call will bind the necessary resources:

common_gl/com.blaster.techniques.TextTechnique::draw

fun draw(call: () -> Unit) {
    glBind(listOf(program, diffuse, rect)) {
        program.setTexture(GlUniform.UNIFORM_TEXTURE_DIFFUSE, diffuse)
        call.invoke()
    }
}

When the resources are bound, we can render characters:

common_gl/com.blaster.techniques.TextTechnique::text

fun text(text: String, start: Vector2f, scale: Float, color: Vector3f) {
    text.forEachIndexed { index, ch ->
        startBuf.set(start.x + index * scale, start.y)
        character(ch, startBuf, scale, color)
    }
}

common_gl/com.blaster.techniques.TextTechnique::character

fun character(ch: Char, start: Vector2f, scale: Float, color: Vector3f) {
    program.setUniform(GlUniform.UNIFORM_CHAR_INDEX, ch.toInt())
    program.setUniform(GlUniform.UNIFORM_CHAR_START, start)
    program.setUniform(GlUniform.UNIFORM_CHAR_SCALE, scale)
    program.setUniform(GlUniform.UNIFORM_COLOR, color)
    rect.draw()
}

We are passing the character color, position, and the index in ASCII table [3] into the shader.

Text font and shader

The shader program is where all the actual work happens:

common_assets/src/main/resources/shaders/text/text.vert

#version 300 es
precision mediump float;

layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec2 aTexCoord;

uniform int uCharIndex;
uniform vec2 uCharStart;
uniform float uCharScale;

out vec2 vTexCoord;

Shader works only with 256×256 textures, but I am not using others, at least for now

const float CHAR_STEP = 16.0 / 256.0;
const int CHARS_IN_ROW  = 16;

This method will calculate the texture coordinates based on the index in the ASCII table and font texture parameters

void calculateTexCoords() {
    float charIndexX = float(uCharIndex % CHARS_IN_ROW);
    float charIndexY = float(CHARS_IN_ROW) - float(uCharIndex / CHARS_IN_ROW) - 1.0;
    float texCoordX = (charIndexX + aTexCoord.x) * CHAR_STEP;
    float texCoordY = (charIndexY + aTexCoord.y) * CHAR_STEP;
    vTexCoord = vec2(texCoordX, texCoordY);
}

The position of the characters on the screen is calculated based on the font scaling

void calculatePosition() {
    float positionX = uCharStart.x + aPosition.x * uCharScale;
    float positionY = uCharStart.y + aPosition.y * uCharScale;
    gl_Position = vec4(positionX, positionY, 0.0, 1.0);
}

void main() {
    calculateTexCoords();
    calculatePosition();
}

And here is the result – we can output the necessary information on the screen:

Debug text

Immediate Mode

Since we are talking about debugging tools, I also wanted to mention OpenGL Immediate Mode briefly.

While it is not recommended due to its low performance, it is quite useful with outputting additional information about the scene. This additional information is never meant to be in the release anyway – therefore, the performance impact is not that noticeable.

But the debug output can simplify life significantly. Imagine debugging scene partitioning solution without seeing the actual intersection between camera frustum and AABBs.

Since the technique is camera based, we need to update the matrices in case the window is resized:

common_gl/com.blaster.techniques.ImmediateTechnique::resize

fun resize(camera: Camera) {
    glCheck {
        backend.glMatrixMode(backend.GL_PROJECTION)
        camera.projectionM.get(bufferMat4)
        backend.glLoadMatrix(bufferMat4)
    }
}

One of the most used methods – drawing of the AABB of the object. Helps in setting the scene correctly:

common_gl/com.blaster.techniques.ImmediateTechnique::aabb

fun aabb(camera: Camera, aabb: AABBf, modelM: mat4, color: vec3 = vec3(1f)) {
    glCheck {

Setting matrices, mode

        backend.glMatrixMode(backend.GL_MODELVIEW)
        val modelViewM = mat4(camera.calculateViewM()).mul(modelM)
        modelViewM.get(bufferMat4)
        backend.glLoadMatrix(bufferMat4)
        backend.glBegin(backend.GL_LINES)

Calculating the vertices of our AABB

        val bottomLeftBack = vec3(aabb.minX, aabb.minY, aabb.minZ)
        val bottomLeftFront = vec3(aabb.minX, aabb.minY, aabb.maxZ)
        val bottomRightBack = vec3(aabb.maxX, aabb.minY, aabb.minZ)
        val bottomRightFront = vec3(aabb.maxX, aabb.minY, aabb.maxZ)
        val topLeftBack = vec3(aabb.minX, aabb.maxY, aabb.minZ)
        val topLeftFront = vec3(aabb.minX, aabb.maxY, aabb.maxZ)
        val topRightBack = vec3(aabb.maxX, aabb.maxY, aabb.minZ)
        val topRightFront = vec3(aabb.maxX, aabb.maxY, aabb.maxZ)

Drawing the lines of AABB

        line(bottomLeftBack, bottomLeftFront, color)
        line(bottomLeftFront, bottomRightFront, color)
        line(bottomRightFront, bottomRightBack, color)
        line(bottomRightBack, bottomLeftBack, color)
        line(topLeftBack, topLeftFront, color)
        line(topLeftFront, topRightFront, color)
        line(topRightFront, topRightBack, color)
        line(topRightBack, topLeftBack, color)
        line(bottomLeftBack, topLeftBack, color)
        line(bottomLeftFront, topLeftFront, color)
        line(bottomRightBack, topRightBack, color)
        line(bottomRightFront, topRightFront, color)
        backend.glEnd()
    }
}

Markers can be used to identify invisible points in the scene – cameras, light sources:

common_gl/com.blaster.techniques.ImmediateTechnique::marker

fun marker(camera: Camera, modelM: mat4, color1: vec3, color2: vec3, color3: vec3, scale: Float = 1f) {
    val half = scale / 2f
    glCheck {

Usual Immediate Mode setup

        backend.glMatrixMode(backend.GL_MODELVIEW)
        val modelViewM = mat4(camera.calculateViewM()).mul(modelM)
        modelViewM.get(bufferMat4)
        backend.glLoadMatrix(bufferMat4)
        backend.glBegin(backend.GL_LINES)

Drawing each line with respect to the center of the marker

        val center = vec3()
        modelM.translation(center)
        val start = vec3()
        val end = vec3()
        start.set(center)
        start.x -= half
        end.set(center)
        end.x += half
        line(start, end, color1)
        start.set(center)
        start.y -= half
        end.set(center)
        end.y += half
        line(start, end, color2)
        start.set(center)
        start.z -= half
        end.set(center)
        end.z += half
        line(start, end, color3)
        backend.glEnd()
    }
}

Each line is drawn with vertices. That is why the mode is called Immediate – we are sending the information about geometry each frame – hence the inefficiency:

common_gl/com.blaster.techniques.ImmediateTechnique::line

private fun line(from: vec3, to: vec3, color: color) {
    backend.glColor3f(color.x, color.y, color.z)
    backend.glVertex3f(from)
    backend.glVertex3f(to)
}

The Teapots scene from the previous post with the debug output enabled:

Debugging visually

Debugging visually

In this article, we discussed the most versatile ways to add information about your scene to the output.

TextTechnique and Console classes allow us to hide the details of drawing fonts in Blaster, and with ImmediateMode, we can quickly draw points and boundaries.

There are many more tricks out there: for example, you can modify the shader output to include the information about the surface: position, normal, time to render, etc. But that approach is more situational.

And that is it – I hope you liked it. Leave your comments below if you want me to cover a topic or to discuss the article. Will see you in the next posts, have a great day!

References