Rendering Demo

In this tutorial we will create a demo application to showcase our sprite renderer. If you have been following the previous tutorials, this demo should be very straightforward and hopefully it will be manageable if you are jumping in here. In this demo we will be showing a set of cards that randomly rotate. When a card is done rotating its face will randomly switch to some other card.

Project Setup

Create an empty folder and inside it initialize a new go project.

go mod init demo

Feel free to call it whatever you want. Create an empty .go file, again call it whatever you want and lets add some code.

Renderer Setup

We need to add some initialization code before we can draw our sprites. We will initialize the following:

All of these have been covered in previous tutorials. You don’t need to understand how they are implemented to use them but if you are curious have a look at what each component does you can read the intro of the corresponding tutorial. Our initialization code is the following:

package main

import (
    "image"
    "time"

    "gitlab.com/onikolas/agl/platform"
    "gitlab.com/onikolas/agl/shaders"
    "gitlab.com/onikolas/agl/sprite"
    "gitlab.com/onikolas/math"
)

func panicOnError(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    err := platform.InitializeWindow(238*6+5, 333*3+2, "demo", true, false)
    panicOnError(err)
    renderer := sprite.NewRenderer()
    renderer.SetBGColor([4]float32{0.1, 0.1, 0.1, 1})
    renderer.EnableBackFaceCulling()
    atlas, err := sprite.NewEmptyAtlas(3000, 2000)
    panicOnError(err)
    shader, err := shaders.NewDefaultShader()
    panicOnError(err)

    for {
    }
}

We begin by creating the application window using the InitializeWindow function from the platform package. We provide the window dimensions, a title, and whether the window should be resizable and fullscreen. The window dimensions are set based on the dimensions of the cards. Each card is 238×333238\times333 pixels and we will be showing 6×36\times3 cards. We also add a pixel of space between each row and column.

Next, we create a renderer with sprite.NewRenderer(). This various structures needed for rendering and will also initialize OpenGL as well.

Afterwards we create a sprite atlas. A sprite atlas is a specialized data structure for storing sprites and efficiently rendering them (see the sprite atlas tutorial to learn more). We specify the size of the atlas to be 3000×20003000\times2000 pixels. We will be storing 56 cards so we need a big atlas.

Our final piece of initialization is to create a shader. A shader is used to give the appearance of a sprite (shade it). For this demo we will use the most basic shader which simply colors the sprite exactly as it appears on the image.

Adding Sprites

We have a set of png images of common playing cards. You can download them here. Each image will be a sprite in our app and we need to load it into our sprite atlas.

To load a sprite we first use the helper function sprite.RgbaFromFile to load the image from disk. Then, we store the loaded image in the sprite atlas using atlas.AddImage.

image := sprite.RgbaFromFile("cards/HEART-1.png")
atlas.AddImage(image)

To load all the images in our cards folder we use the os package to get a list of filenames under that folder.

dirContents, err := os.ReadDir("cards/")
panicOnError(err)
imageFilenames := []string{}
for _, e := range dirContents {
    imageFilenames = append(imageFilenames, "cards/"+e.Name())
}

We then load all images.

images := []*image.RGBA{}
for i := range imageFilenames {
    img, err := sprite.RgbaFromFile(imageFilenames[i])
    panicOnError(err)
    images = append(images, img)
}

And pass these images to the atlas.

ids, err = atlas.AddImages(images)

Atlas can load images one-by-one (using atlas.AddImage) or all at once as we did above. Loading all at once is more efficient so we prefer it whenever possible. We can examine what the atlas looks like using atlas.DumpAtlas(). This will save an image called atlas-image.png in the same folder where we run our program.

Now our sprites are loaded in the application and we are ready to render. In a game, this process would be done during the level loading.

Rendering a Sprite

To render one of the sprites we have in our atlas we must create a Sprite. Sprites in the atlas are identified with an id which is a simple integer. When a sprite is added to the atlas with atlas.AddImage, the function returns that sprite’s id.

spriteID, err := atlas.AddImage(someImage) 

When we add multiple images with atlas.AddImages we get a list of ids. The order of the ids matches the order in which we added the images to the atlas. Furthermore, the sprite ids are predictable. The first sprite to be added to the atlas gets id=0, the second id=1 and so on. When we loaded our card images to the atlas we used os.ReadDir which lists files in alphabetical order1. Running ls -l cards/ lets us know that id=0 is the BACK card, id=1 is CLUB-1 and so on.

To create a sprite we need the sprite id, the atlas that it came from and a shader that will be used to shade the sprite:

club1Sprite, err := sprite.NewSprite(1, atlas, &shader, 1)

We can modify many of the sprite’s parameters like its size, position and rotation. The default settings for the sprite would have it appear at position (0,0)(0,0) which is the lower-left of the application window. Lets set it somewhere near the center.

club1Sprite.SetPosition(math.Vector3[float32]{700, 500, 0})

We can now render this sprite. We will first queue the sprite and then call renderer.Render.

for {
    renderer.QueueRender(&club1Sprite)
    renderer.Render()
}

In a game we would be queuing all the active sprites in the level and then calling render once to draw all of them. We can now build the code and see our sprite!

Building

If you build this code (with go build) you will get errors because the packages that we included have not been pulled to the project. Go can pull git dependencies automatically by running:

go get -u -v

You will see that pulls a few other packages automatically. These are dependencies of the included packages. When the packages have been added you should be able to build the code. The first build triggers the compilation of the OpenGL library which is large and written in C so it might take a while. After you build the code once, compilation only processes Go code and should be much faster.

Running the demo should give you this:

Ace!

Rotating Cards

To make a playing card we will create two sprites at the same location and rotate one of them 180 degrees on the Y axis so that they are facing in opposite directions. One of the sprites will be the card back and the other sprite will be the card face. Lets define a Card type to make this easier.

type Card struct {
    FrontSprite, BackSprite sprite.Sprite
    Rotating                bool
}

Cards are created with the NewCard function:

func NewCard(postition math.Vector3[float32], atlas *sprite.Atlas, shader *shaders.Shader) Card {
    back, _ := sprite.NewSprite(0, atlas, shader, 1)
    front, _ := sprite.NewSprite(rand.Int()%55+1, atlas, shader, 1)
    card := Card{
        FrontSprite: front,
        BackSprite:  back,
        Rotating:    false,
    }
    card.FrontSprite.SetPosition(postition)
    card.BackSprite.SetPosition(postition)
    card.FrontSprite.SetRotation(math.Vector3[float32]{0, 3.14159, 0})
    return card
}

The function creates two sprites, one for each face of the card. BackSprite always gets id=0 and FrontSprite gets a random id in the range [1,55][1,55]. Both sprites are moved to the position passed in the position parameter.

Cards are rotated by calling their Update method. On every call to Update there is a one in a thousand chance for the card to start rotating. Update gets called multiple times per second so cards start rotating all the time.

func (c *Card) Update() {
    if c.Rotating {
        rotation := c.FrontSprite.GetRotation()
        c.FrontSprite.SetRotation(rotation.Add(math.Vector3[float32]{0, 0.01, 0}))
        rotation = c.BackSprite.GetRotation()
        c.BackSprite.SetRotation(rotation.Add(math.Vector3[float32]{0, 0.01, 0}))

        // at 360 degrees reset the card
        if math.Equals[float32](rotation.Y, 2*3.14159, 0.02) {
            c.Rotating = false
            c.BackSprite.SetRotation(math.Vector3[float32]{0, 0, 0})
            c.FrontSprite.SetRotation(math.Vector3[float32]{0, 3.14159, 0})
            c.FrontSprite.SetIndex(rand.Int()%55 + 1)
        }
    } else {
        if rand.Int()%1000 < 1 {
            c.Rotating = true
        }
    }
}

Once a card is set to rotate, we continually increase its Y rotation until it completes a 360 degree rotation. At this point we stop rotating, and change the front sprite to some other face randomly.

Creating Multiple Sprites

In our main function we will create a grid of 6×36\times3 cards. We begin by grabbing the card size.

cardSize := math.Vector2ConvertType[int, float32](atlas.GetBoundingBox(0).Size())

We know this size by opening one of the cards in an image editor but we add this here to show how it can be done in code. We then loop 18 times and create cards. We start placing the first card at the bottom-left of the screen. Since our sprites are centered at their middle, we offset the first sprite by half it’s size. We then continue to add sprites, each time placing the spite at cardSize.X+1 distance from the previous one.

cards := []Card{}
pos := math.Vector3[float32]{cardSize.X / 2, cardSize.Y / 2, 0}
screenBox := renderer.GetScreenBoundingBox()
for i := 0; i < 18; i++ {
    card := NewCard(pos, atlas, &shader)
    cards = append(cards, card)
    pos = pos.Add(math.Vector3[float32]{cardSize.X + 1, 0, 0})
    if pos.X > screenBox.P2.X {
        pos.X = cardSize.X / 2
        pos.Y += cardSize.Y + 1
    }
}

If we reach the end of the window we increase the Y coordinate by cardSize.Y+1 and reset 8 back to the left side. This process fills the whole window with cards.

Rendering

Rendering the cards is straightforward. We loop over all cards and call the card.Update() method to update each card. This might cause it to start rotating, continue to rotate it if its already in motion or reset if it completed a full rotation. We then queue both the back and front sprites to be rendered.

for {
    for i := range cards {
        cards[i].Update()
        renderer.QueueRender(&cards[i].BackSprite)
        renderer.QueueRender(&cards[i].FrontSprite)
    }
    renderer.Render()
    time.Sleep(time.Millisecond * 10)
}

At any point only one of the sprites will actually render for each card. We enforce this by setting the renderer option:

renderer.EnableBackFaceCulling()

This makes it so that the backs of our sprites never show. Alternatively, we could check the Y rotation value of each sprite and decide which one to queue at each update. This exercise is left to the reader.

Further Work

This concludes our demo. Feel free to play around with it. Here are some ideas to try out (from easy to hard):