Sprite animation with matobject

When doing 2D games or sprite effects you can encode multiple states or images in a single image, and later use sub-regions. For rendering performance such "image atlases" are quite good, as less texture switches are better for performance. In this tutorial we will draw a textured quad with a explosion animation. If you have huge amounts of sprites or more complex effects, you should use the particle system for optimal performance, but for less complex scenes creating l3dnodes for each object is no big deal either.

Our Texture holds animation of a single explosion

Scene Setup

First we setup view/camera, this time we will use orthographic projection, like in most sprite games.

view = UtilFunctions.simplerenderqueue()
view.rClear:colorvalue(0.1,0.1,0.1,0)


----------------------------------
-- lets create a orthographic view
camact = actornode.new("cam")
cam = l3dcamera.default()
-- negative fov means orthographic and the amount is the 
-- "width" in units
cam:fov(-64)
cam:linkinterface(camact)
-- rotate the camera in such a way that
-- we look at the X,Y plane, -Z becomes depth
camact:rotdeg(-90,0,0)

Sprite Object

For the textured quad object we use a l3dprimitive.newquadcentered, which is a single quad object with the center in the middle of the rectangle. With the rendersurface interface we setup blendmode to allow transparency effect, and with renderflag we activate blending.

-- load the texture
tex = texture.load("explosprite.png")

-- create actor
act = actornode.new("spr")
-- move it a bit to the left
act:pos(-5,0,-50)

spr = l3dprimitive.newquadcentered("spr",32,32)
spr:linkinterface(act)
spr:matsurface(tex)
-- lets blend the quad as decal (ie Alpha channel
-- becomes transparency)
spr:rsBlendmode(blendmode.decal())
spr:rfBlend(true)

Sprite Animation

Many meshes that can have materials/textures assigned also support the matobject interface. With it you can individually further control material effects for each renderable mesh. For sprites we need to modify the texture matrix. That matrix transforms the meshes' texture coordinates on vertex level before texture sampling is done later on pixel level. You can stretch/scale / rotate / shift... the texture that way. For the sprites we want to pick a subregion of the bigger image. Be aware that powerof2 textures have always dimensions of [0,1] while rectangle textures (special capability & instructions needed) need coordinates in [0,size] to show the full texture. Normally you will have powerof2 textures, that means if you want to move by a single pixel you move by "1/size".

We create a function that sets up texture matrix for a object that implements the matobject interface.

function choosesprite(mo,n,tex,imgcntW,imgcntH)
    local w,h = tex:dimension()
    n = (n or 0)%(imgcntW*imgcntH)
    local x,y = n%imgcntW, math.floor(n/imgcntW)

    -- We must divide the coordinates by the image dimension
    -- to get into the [0,1] space.
    mo:moRotaxis(
      1/imgcntW,0,0,
      0,1/imgcntH,0,
      0,0,1)

    -- for the position offset we also have to 
    -- flip Y axis, because OpenGL defines 0,0 as bottom left
    -- And we assume our sprites to start at top left.
    mo:moPos(x/imgcntW,1-y/imgcntH-1/imgcntH,0)
end

Later in our think method we advance the subrectangle.

local n = 0
function anim ()
    -- hand over 
    -- 1. our object (spr)
    -- 2. the subrectangle we want (starting top left)
    -- 3. the texture we use
    -- 4/5. the grid size of subrectangles stored

    choosesprite(spr,n,tex,4,4)
    print(n%16)
    n = n + 1
end

-- lower the last value to speed up the playback
Timer.set("tutorial",anim,100)