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.
The following is our Sprite
type:
type Sprite struct {
int
index int
renderOrder *Atlas
atlas *shaders.Shader
shader []*shaders.ShaderAttribute
shaderData int
bufferIndex }
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.
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) {
:= Sprite{
spr : spriteId,
index: []*shaders.ShaderAttribute{},
shaderData: spriteAtlas,
atlas: shader,
shader: -1, // negative means the renderer hasn't assigned a BufferList for us yet
bufferIndex: renderOrder,
renderOrder}
// sort shader attributes by their location, this way sprite attributes order implicitly matches
// BufferList arrays which are sorted in the same way
:= ds.SortMapByValue(shader.Attributes, func(a, b shaders.ShaderAttribute) bool {
attrKeys return a.Location < b.Location
})
// allocate arrays for each attribute
for _, v := range attrKeys {
:= shader.Attributes[v]
attr if attr.Name == "vertex" {
continue
}
:= attr.Copy()
newAttr .shaderData = append(spr.shaderData, &newAttr)
spr}
.SetIndex(spriteId)
spr
// set original size as the default
.SetOriginalSize()
spr
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:
[]math.Box2D[int] spriteBoundingBoxes
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) {
.index = index
s:= s.atlas.GetSpriteUVs(index)
uvs 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 pixels on the atlas it will appear as 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.
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.
, _ := sprite.NewAtlas(image.NewRGBA(image.Rect(0, 0, 1024, 1024)), nil) # create atlas
atlas, _ := atlas.AddImage(some_image) # add image
index, _ := sprite.NewSprite(index, atlas, &some_shader, 1) # make a sprite 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 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.
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 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
.
Method SetPosition
sets a sprite’s position given
a 3D vector as parameter. These values are in pixels. By default,
we map position
to the bottom-left corner of the window but this can be changed in
the renderer.
func (s *Sprite) SetPosition(position math.Vector3[float32]) {
.shaderData[translationIndex].Data[0] = position.X
s.shaderData[translationIndex].Data[1] = position.Y
s.shaderData[translationIndex].Data[2] = position.Z
s}
Variable translationIndex
is part of an
enumeration that we setup to enable quick access to common sprite
parameters:
const (
= iota
uvIndex
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]{
: s.shaderData[translationIndex].Data[0],
X: s.shaderData[translationIndex].Data[1],
Y: s.shaderData[translationIndex].Data[2],
Z}
}
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]) {
.shaderData[rotateIndex].Data[0] = rot.X
s.shaderData[rotateIndex].Data[1] = rot.Y
s.shaderData[rotateIndex].Data[2] = rot.Z
s}
// Setter for sprite scale.
func (s *Sprite) SetScale(scale math.Vector2[float32]) {
.shaderData[scaleIndex].Data[0] = scale.X
s.shaderData[scaleIndex].Data[1] = scale.Y
s}
// Set the sprite color. The same color is applied to every vertice.
func (s *Sprite) SetColor(color color.ColorRGBA) {
.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
s}
Their equivalent getters are similarly simple:
func (s *Sprite) GetRotation() math.Vector3[float32] {
return math.Vector3[float32]{
: s.shaderData[rotateIndex].Data[0],
X: s.shaderData[rotateIndex].Data[1],
Y: s.shaderData[rotateIndex].Data[2],
Z}
}
func (s *Sprite) GetScale() math.Vector2[float32] {
return math.Vector2[float32]{
: s.shaderData[scaleIndex].Data[0],
X: s.shaderData[scaleIndex].Data[1],
Y}
}
func (s *Sprite) GetColor() color.ColorRGBA {
return color.NewColorRGBAFromSlice(s.shaderData[colorIndex].Data[0:4])
}
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()
}
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) {
.index = index
s:= s.atlas.GetSpriteUVs(index)
uvs 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.
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++ {
.shaderData[location].Data[i] = data[i]
s}
}
// 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
}