Animation

In this tutorial we will build on our previous code to create a screensaver. Our screensaver will move a sprite around on the screen and the sprite itself will animate. Let’s start with moving the sprite.

Moving Geometry

To move our sprite we must move its vertices. We already know how to pass vertex data to the GPU using the gl.BufferData. To make our sprite move we can update our triangle data in a loop.

var triangles = [18]float32{}
x, y := float32(-1), float32(-1)
for {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    x += 0.001
    y += 0.001
    triangles[0], triangles[1], triangles[2] = x, y, 0
    triangles[3], triangles[4], triangles[5] = x+1, y, 0
    triangles[6], triangles[7], triangles[8] = x, y+1, 0
    triangles[9], triangles[10], triangles[11] = x, y+1, 0
    triangles[12], triangles[13], triangles[14] = x+1, y, 0
    triangles[15], triangles[16], triangles[17] = x+1, y+1, 0
    gl.BindBuffer(gl.ARRAY_BUFFER, vertexVbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(triangles), gl.Ptr(&triangles[0]), gl.DYNAMIC_DRAW)
    gl.DrawArrays(gl.TRIANGLES, 0, 6)
    window.GLSwap()
}

This will make the sprite move upwards and to the right. You can use the code from tutorial 1 and add the code above. Play around with the initial value of x and y to set the sprite’s starting position. Also, you can replace the +1 in the triangles definition to a variable and that will let you control the sprite’s size. Try to add some code to make the sprite bounce when it reaches the end of the screen.

Animating the Sprite

To animate we need to show different images in quick succession. We can load multiple images into textures and then switch which one we show in our render loop. To load textures we will reuse the code from the previous tutorial:

func imageToTexture(imgFilename string) uint32 {
    imgFile, err := os.Open(imgFilename)
    if err != nil {
        return 0
    }
    img, _, err := image.Decode(imgFile)
    if err != nil {
        return 0
    }

    rgba := image.NewRGBA(img.Bounds())
    if rgba.Stride != rgba.Rect.Size().X*4 {
        return 0
    }
    draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)

    var texture uint32
    gl.GenTextures(1, &texture)
    gl.BindTexture(gl.TEXTURE_2D, texture)
    gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    //gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.TexImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        int32(rgba.Rect.Size().X),
        int32(rgba.Rect.Size().Y),
        0,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        gl.Ptr(rgba.Pix))

    return texture
}

The above loads an image, decodes it into RGBA format and then creates an OpenGL texture out of it. We use imageToTexture to load all of our images into textures.

textures := [3]uint32{
    imageToTexture("utah1.png"),
    imageToTexture("utah2.png"),
    imageToTexture("utah3.png"),
}

Then in our loop switch the rendered texture on every iteration:

for {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    // Triangle setup
    // ...
    gl.BindBuffer(gl.ARRAY_BUFFER, vertexVbo)
    gl.BindTexture(gl.TEXTURE_2D, textures[texIndex])
    texIndex = (texIndex + 1) % 3
    gl.DrawArrays(gl.TRIANGLES, 0, 6)
    window.GLSwap()
}

This will make the texture animate as fast as the loop is able to run which is likely too fast. We can add a pause to our loop to actually see the animation with time.Sleep(time.Second).

Check the code up to this point here. A nice challenge is to time the main loop so that the sprite animation changes once per second but the sprite movement continues without pause. Properly timing the loop is the subject of the next tutorial.

The above approach works but it is not ideal for building a 2D sprite engine. In a 2D game there can be hundreds if not thousands of sprites and loading each one into its own texture is not efficient. To draw each sprite we must call gl.BindTexture to enable it’s corresponding texture. This means that all the sprites in our VBO will be drawn with the same texture and to draw different sprites we must split them into different VBOs. Here is some pseudocode for this approach:

for {
    for texture, VBO in spriteTypes {
        gl.BindBuffer(gl.ARRAY_BUFFER, VBO)
        gl.BufferData(...)
        gl.BindTexture(gl.TEXTURE_2D, texture)
        gl.DrawArrays(gl.TRIANGLES, 0, 6)
    }
    window.GLSwap()
}

Transfers to the GPU have high bandwidth but also high latency so its better to have one big transfer than multiple small ones so that we don’t pay the latency price multiple times. With that in mind, to get the best performance we must reduce the number of calls that transfer data to the GPU (BufferData). Ideally, we want a single call to BufferData that moves all the vertex info to the GPU, a single texture bind and a single draw call (DrawArrays). To achieve this we will concatenate all of our sprites into a single texture and then use uv mapping to swap sprites. Here are our sprites from before, concatenated into a single image.

We call this a sprite atlas or sprite sheet. We can render sprites on this atlas by setting texture coordinates appropriately. For example, the uvs for sprite one are:

var uvs = [12]float32{
    0, 1,
    0.333, 1,
    0, 0,
    0, 0,
    0.333, 1,
    0.333, 0,
}

Remember that each pair of values corresponds to one vertex. Mapping image two would require:

var uvs = [12]float32{
    0.333, 1,
    0.666, 1,
    0.333, 0,
    0.333, 0,
    0.666, 1,
    0.666, 0,
}

And three:

var uvs = [12]float32{
    0.666, 1,
    1, 1,
    0.666, 0,
    0.666, 0,
    1, 1,
    1, 0,
}

We can combine all the uvs in one array.

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

    0.666, 1,
    1, 1,
    0.666, 0,
    0.666, 0,
    1, 1,
    1, 0,
}

And now in our render loop instead of swapping textures we swap uvs.

x, y := float32(-1), float32(-1)
uvIndex := 0
for {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    
    // Update vertices
    // ...
    
    gl.BindBuffer(gl.ARRAY_BUFFER, vertexVbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(triangles), gl.Ptr(&triangles[0]), gl.DYNAMIC_DRAW)

    gl.BindBuffer(gl.ARRAY_BUFFER, uvVbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(uvs), gl.Ptr(uvs[uvIndex*12:(uvIndex+1)*12]), gl.DYNAMIC_DRAW)
    uvIndex = (uvIndex + 1) % 3

    gl.DrawArrays(gl.TRIANGLES, 0, 6)
    window.GLSwap()
    time.Sleep(time.Second)
}

Perhaps the most confusing part of the code is this which uses a mechanism called slicing.

gl.BufferData(gl.ARRAY_BUFFER, 4*len(uvs), gl.Ptr(uvs[uvIndex*12:(uvIndex+1)*12]), gl.DYNAMIC_DRAW)
uvIndex = (uvIndex + 1) % 3

The above code makes it so that every second we map a different portion of the uvs array into our uvVbo. The slice [uvIndex*12:(uvIndex+1)*12] will resolve to:

[0:12]  when uvIndex=0
[12:24] when uvIndex=1
[24:36] when uvIndex=2

The line uvIndex = (uvIndex + 1) % 3 uses modulo, which is the remainder of integer division, to cause uvIndex to rotate between the values 0,1 and 2:

0 % 3 // 0
1 % 3 // 1
2 % 3 // 2
3 % 3 // 0 

The benefit to this approach is that we can now have multiple sprites showing different textures in the same VBO and we can draw all of them with a single draw call1. Lets add another sprite to illustrate.

// triangle 1
triangles[0], triangles[1], triangles[2] = x, y, 0
triangles[3], triangles[4], triangles[5] = x+1, y, 0
triangles[6], triangles[7], triangles[8] = x, y+1, 0
triangles[9], triangles[10], triangles[11] = x, y+1, 0
triangles[12], triangles[13], triangles[14] = x+1, y, 0
triangles[15], triangles[16], triangles[17] = x+1, y+1, 0
// triangle 2
triangles[18], triangles[19], triangles[20] = 0, y, 0
triangles[21], triangles[22], triangles[23] = 1, y, 0
triangles[24], triangles[25], triangles[26] = 0, y+1, 0
triangles[27], triangles[28], triangles[29] = 0, y+1, 0
triangles[30], triangles[31], triangles[32] = 1, y, 0
triangles[33], triangles[34], triangles[35] = 1, y+1, 0

gl.BindBuffer(gl.ARRAY_BUFFER, vertexVbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(triangles), gl.Ptr(&triangles[0]), gl.DYNAMIC_DRAW)

gl.BindBuffer(gl.ARRAY_BUFFER, uvVbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(uvs), gl.Ptr(uvs[uvIndex*12:(uvIndex+2)*12]), gl.DYNAMIC_DRAW)
uvIndex = (uvIndex + 1) % 2

Notice that we are slicing the uvs array to get 24 indices. We could of also set the uvs array manually like we do with the triangles. To solidify your understanding change the code so that the first sprite counts forward (1,2,3) and the second counts in reverse (3,2,1). Use the example code as a starting point.

This technique of bundling sprites into a large sprite atlas will be central to the design of our sprite engine.