Sprite

Sprite is the object that holds the data needed to render a sprite. We already saw in the buffers tutorial that Sprite holds rendering data that we copy to BufferList. Unlike BufferList which is managed by the engine itself, Sprite is a struct that our users will be creating and using directly so we aim to provide easy to use functions for it.

Structure

The following is our Sprite type:

type Sprite struct {
    index        int
    renderOrder  int
    atlas        *Atlas
    shader       *shaders.Shader
    shaderData   []*shaders.ShaderAttribute
    bufferIndex  int
}

The Sprite structure holds references to the structures needed to render it: the sprite atlas that holds the sprite’s uvs and the shader that will do the shading. A sprite has an index which uniquely identifies it in the sprite atlas. Also, a sprite has a renderOrder which is the order in which Renderer will process this sprite. A sprite holds buffers for each shader attribute of shader.

Notice that all attributes of Sprite start with a lowercase letter. This is Go’s equivalent of a private field and it is not accessible to users - it’s only accessible to the library itself. We do this to force users to use our setter functions which ensure that shader data is correctly set.

Constructor

The Sprite constructor creates a sprite given a sprite atlas, a shader and the sprite’s id (we discuss parameter renderOrder in the next section).

func NewSprite(spriteId int, spriteAtlas *Atlas, shader *shaders.Shader, renderOrder int) (Sprite, error) {
    spr := Sprite{
        index:       spriteId,
        shaderData:  []*shaders.ShaderAttribute{},
        atlas:       spriteAtlas,
        shader:      shader,
        bufferIndex: -1, // negative means the renderer  hasn't assigned a BufferList for us yet
        renderOrder: renderOrder,
    }

    // sort shader attributes by their location, this way sprite attributes order implicitly matches
    // BufferList arrays which are sorted in the same way
    attrKeys := ds.SortMapByValue(shader.Attributes, func(a, b shaders.ShaderAttribute) bool {
        return a.Location < b.Location
    })

    // allocate arrays for each attribute
    for _, v := range attrKeys {
        attr := shader.Attributes[v]
        if attr.Name == "vertex" {
            continue
        }
        newAttr := attr.Copy()
        spr.shaderData = append(spr.shaderData, &newAttr)
    }

    spr.SetIndex(spriteId)

    // set original size as the default
    spr.SetOriginalSize()

    return spr, nil
}

The constructor initializes the shaderData array which holds the data for shader attributes (position, uv, color etc). Just like BufferList, we sort these attributes by their location so we guarantee that when the sprites are added to the BufferList via BufferList.AddSprite everything gets copied correctly.

Next, we must assign the sprite’s UV which come from the sprite atlas. If you recall, sprite atlas stores the locations of its sprites in a list:

spriteBoundingBoxes[]math.Box2D[int]

Parameter spriteId is the index into the above array. This information is used to create the UV coordinates of the sprite using SetIndex. This is a setter function that assigns the id to the index field of Sprite and pulls the uv data from the atlas using the same key to store it. This function can also be used to switch a sprite to another sprite in the atlas.

func (s *Sprite) SetIndex(index int) {
    s.index = index
    uvs := s.atlas.GetSpriteUVs(index)
    copy(s.shaderData[uvIndex].Data, uvs[:])
}

The last piece of initialization is to set the sprite its original size. This way, if a sprite takes 20×4020\times40 pixels on the atlas it will appear as 20×4020\times40 pixels when we render. This is just a default and the user can set whatever size they want using the accessor functions that we will see in a bit.

Parameter bufferIndex is used by the renderer and will be explained in the renderer tutorial.

Initializing from an Image

Our convention is that sprites can only come from a sprite atlas so if the user wants to render images they have to first add them to an atlas and then create a sprite.

atlas, _ := sprite.NewAtlas(image.NewRGBA(image.Rect(0, 0, 1024, 1024)), nil) # create atlas
index, _ := atlas.AddImage(some_image)                                        # add image
sprite, _ := sprite.NewSprite(index, atlas, &some_shader, 1)                  # make a sprite

Of course this can be automated and we can even provide a service that creates atlases automatically. This will be covered in an upcoming tutorial.

Render Order

Render order is used to define a group of sprites that all get rendered together. For example, all sprites with render order 1 could be background sprites and sprites with render order 2 could be foreground. There is no explicit ordering for sprites within the same render order. Render order is important for transparent sprites, like the letters seen on the left image below. Such sprites must be rendered after the sprites behind them otherwise transparency won’t work. In this example the sprites that make up the three letters must have higher render order than the sprite that makes the grey background. They must also be closer to the camera meaning they must have a smaller depth value.

Transparent Cutout

Render order is not important for cutout sprites such as the letters seen on the image to the right. Here, transparency is binary meaning that it is either fully opaque (surface of the letters) or fully transparent (area around the letters). Depth is still important, and the depth value for the foreground letters must be smaller than the background. Cutout sprites are the most common and if a game uses only cutout sprites render order can be ignored (set it to 0 for every sprite).

As we will see later, the renderer creates a BufferList for each render order so its important to not assign unnecessary render order values if they are not needed as it will negatively impact performance.

Setters

Setters are functions that set the field values of the Sprite struct. In the Go coding style, we do not normally use setters and instead expose the fields for direct access. Here we break this convention because setting shader data directly is error prone and inconvenient. We will go over the sprite setter functions in this section starting with SetPostition.

SetPosition

Method SetPosition sets a sprite’s position given a 3D vector as parameter. These values are in pixels. By default, we map position (0,0)(0,0) to the bottom-left corner of the window but this can be changed in the renderer.

func (s *Sprite) SetPosition(position math.Vector3[float32]) {
    s.shaderData[translationIndex].Data[0] = position.X
    s.shaderData[translationIndex].Data[1] = position.Y
    s.shaderData[translationIndex].Data[2] = position.Z
}

Variable translationIndex is part of an enumeration that we setup to enable quick access to common sprite parameters:

const (
    uvIndex = iota
    colorIndex
    translationIndex
    rotateIndex
    scaleIndex
)

This enumeration lets us change shaderData in constant time. This is important as we are likely to change some of the parameters in every frame. A moving sprite, for example, changes its position every frame. The above enumeration assumes that sprite attributes are at fixed locations. We enforce this (very loosely!) by setting the same order in Shader.

var CommonAttributes = []string{"vertex", "uv", "color", "translation", "rotation", "scale"}

Since position is not exposed to the user, to get it we must provide a method. For position this is unsuprisingly GetPosition:

func (s *Sprite) GetPosition() math.Vector3[float32] {
    return math.Vector3[float32]{
        X: s.shaderData[translationIndex].Data[0],
        Y: s.shaderData[translationIndex].Data[1],
        Z: s.shaderData[translationIndex].Data[2],
    }
}

SetRotation, SetScale, SetColor

These setters just assign the passed value. The only benefit of having a setter function for these is that we can pass the value in a convenient vector type and it gets assigned to the correct shader array position.

// Setter for sprite rotation
func (s *Sprite) SetRotation(rot math.Vector3[float32]) {
    s.shaderData[rotateIndex].Data[0] = rot.X
    s.shaderData[rotateIndex].Data[1] = rot.Y
    s.shaderData[rotateIndex].Data[2] = rot.Z
}

// Setter for sprite scale.
func (s *Sprite) SetScale(scale math.Vector2[float32]) {
    s.shaderData[scaleIndex].Data[0] = scale.X
    s.shaderData[scaleIndex].Data[1] = scale.Y
}

// Set the sprite color. The same color is applied to every vertice.
func (s *Sprite) SetColor(color color.ColorRGBA) {
    s.shaderData[colorIndex].Data[0] = color.R
    s.shaderData[colorIndex].Data[1] = color.G
    s.shaderData[colorIndex].Data[2] = color.B
    s.shaderData[colorIndex].Data[3] = color.A
}

Their equivalent getters are similarly simple:

func (s *Sprite) GetRotation() math.Vector3[float32] {
    return math.Vector3[float32]{
        X: s.shaderData[rotateIndex].Data[0],
        Y: s.shaderData[rotateIndex].Data[1],
        Z: s.shaderData[rotateIndex].Data[2],
    }
}

func (s *Sprite) GetScale() math.Vector2[float32] {
    return math.Vector2[float32]{
        X: s.shaderData[scaleIndex].Data[0],
        Y: s.shaderData[scaleIndex].Data[1],
    }
}

func (s *Sprite) GetColor() color.ColorRGBA {
    return color.NewColorRGBAFromSlice(s.shaderData[colorIndex].Data[0:4])
}

SetOriginalSize

SetOriginalSize function scales the sprite to its dimensions (in pixels) as it appears on the sprite atlas.

func (s *Sprite) SetOriginalSize() {
    orig := math.Vector2ConvertType[int, float32](s.GetOriginalSize())
    s.SetScale(orig)
}

GetOriginalSize is the equivalent getter.

func (s *Sprite) GetOriginalSize() math.Vector2[int] {
    bb := s.atlas.GetBoundingBox(s.index)
    return bb.Size()
}

SetIndex

This function allows us to switch the sprite to some other sprite on the atlas. Since the sprite doesn’t hold any image data itself, this is a very cheap operation as it just changes the shader values of the uv coordinates.

func (s *Sprite) SetIndex(index int) {
    s.index = index
    uvs := s.atlas.GetSpriteUVs(index)
    copy(s.shaderData[uvIndex].Data, uvs[:])
}

This function can be used to create an animated sprite by keeping a list of indexes and switching between them.

Generic Setter and Getter

Shaders can have attributes other than the common ones that we enumerated. To get and set these we use these methods:

// General setter for sprite attributes.
func (s *Sprite) SetAttribute(location int, data []float32) {
    if location >= len(s.shaderData) || location < 0 {
        return
    }
    for i := 0; i < math.Min[int](len(s.shaderData[location].Data), len(data)); i++ {
        s.shaderData[location].Data[i] = data[i]
    }
}

// General getter for sprite attributes. Does not copy so use with care.
func (s *Sprite) GetAttribute(location int) []float32 {
    if location >= len(s.shaderData) || location < 0 {
        return nil
    }
    return s.shaderData[location].Data
}