Rendering Architecture

To properly organize our code, we will create stucts to hold our data and make functions that operate on that data into methods. We will start by looking at the example code from one of the previous tutorials and try to identify code that should be placed in reusable structures.

We will use the game loop tutorial as our base. The first thing we did in tha example (and all our examples so far) is to initialize a window using SDL. This is boilerplate code that we can put in a function. In AGL, SDL code is in the platform sub package.

Next we setup various OpenGL parameters:

gl.Init()
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Enable(gl.BLEND)
...

This are rendering settings and will go in a Renderer struct. Afterwards we define uvs for our sprites.

var uvs = [36]float32{
    0, 1,
    0.333, 1,
    0, 0,...

Remember that uvs are coordinates on the atlas texture. We load the atlas image and convert it to a texture a few lines later:

texture := imageToTexture("utahs.png")
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, texture)

Since uv coordinates point to locations on a textures, it makes sense then to group uvs and the atlas texture into one structure (we will call it Atlas). We already have a function to load the image and convert it to an OpenGL texture (imageToTexture) and that function will become a method of Atlas.

Next, we define OpenGL buffers to hold our data (triangles and uvs) and this part can also be abstracted. Remember that we create buffers depending on the data that our shaders require. In the game loop tutorials our shaders needed two inputs, vertices and uvs:

layout (location=0) in vec3 vertex;
layout (location=1) in vec2 uv; 

So, we created two vertex buffers (vertexVbo, uvVbo in the code). When we want to render using a shader we bind all the buffers needed for that shader. It makes sense then to define these buffers in groups and not individually as they will always be binded together. We will call the object that holds our buffers a BufferList. Our final piece of setup code was to load our shaders. This is already in a function called createShaderProgram and we will expand it further into a Shader struct.

With that, setup is finished and we go in a loop where we do our rendering. Rendering consists of changing sprite parameters (position and uvs), binding the buffers and copying the new data to the GPU (via gl.BufferData) and finally rendering using gl.DrawArrays. Sprite parameters will go in a struct called Sprite (gasp!) and the rest of the loop will become a method of our Renderer struct.

To summarize, in the following tutorials we will define these structures:

We will go over these starting with Atlas, as it has the least amount of dependencies and gradually build to a complete rendering system. To keep things clean and compartmentalized, we will define our rendering system independently of any game-related structures (we will not implement loop timing for example). Afterwards when we build our game engine, the rendering system will be an included dependency. This also allows us to use the rendering system in other applications other than games.