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:
- Atlas
- Shader
- BufferList
- Sprite
- Renderer
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.