Sprite Sheet shader

I know there are some other solutions to this problem, and I haven’t looked around to compare performance… the below example uses some Zelda textures nicked off the web. The sprite sheet is loaded to the mesh and the mesh is a simple rectangle at the sprite size, then you can tell it to draw any tile off the sprite sheet by x,y being 0,0 from the bottom left. The example runs 15fps on my ipad 2 and it’s doing 676 sprites per frame, all sprites addressed on sprite sheets.

(It’s supposed to look rubbish :wink: )


--# Main
-- SpriteSheet

function didGetBackImage(image)
    landTiles = SpriteSheet(image, vec2(32,32), vec2(30,16))
    --30x16 tiles
    asynchLoadStatus = asynchLoadStatus + 1
    print("got backgrounds")
end

function didGetPeopleImage(image)
    peopleTiles = SpriteSheet(image, vec2(48,64), vec2(12,8))
    --12x8 tiles
    asynchLoadStatus = asynchLoadStatus + 1
    print("got people")
end

-- Use this function to perform your initial setup
function setup()
    asynchLoadStatus = 0
    
    --create a mesh 
    http.request("http://ssvcs.s3.amazonaws.com/ZeldaBackgroundSS.png", didGetBackImage)
    
    http.request("http://ssvcs.s3.amazonaws.com/ZeldaSS.png", didGetPeopleImage)
    
    --let's set up a world
    world = {}
    for x=1,24 do
        for y = 1,24 do
            table.insert(world, {location = vec2(x*32, y*32), tile = vec2(math.random(30)-1, math.random(16)-1)})
        end
    end
    
    --let's add some people
    people = {}
    for i=1, 100 do
        table.insert(people, {char = vec2(math.random(4)-1, math.random(2)-1), direction = math.random(4)-1, location = vec2(math.random(WIDTH), math.random(HEIGHT))})
    end    
    
    frame = 0
end

-- This function gets called once every frame
function draw()
    if asynchLoadStatus < 2 then
        print("not yet")
        return
    end
    
    print(1/DeltaTime)
    -- This sets a dark background color 
    background(40, 40, 50)
    
    --draw the world
    for k,v in ipairs(world) do
        resetMatrix()
        translate(v.location.x, v.location.y)
        landTiles:draw(v.tile)
    end
    
    for k,v in ipairs(people) do
        resetMatrix()
        translate(v.location.x, v.location.y)
        peopleTiles:draw(vec2(v.char.x*3+frame, v.char.y*4+v.direction)) 
    end
    frame = (frame + 1) % 3
end


--# SpriteSheet
SpriteSheet = class()

function SpriteSheet:init(sheetImage, size, tiles)
    self.m = mesh()
    self.m.texture = sheetImage
    self.m:addRect(-size.x/2, -size.y/2, size.x, size.y)
    self.m.shader = shader(SpriteSheetShader.vertexShader, SpriteSheetShader.fragmentShader)
    self.m.shader.tiles = tiles
end

function SpriteSheet:draw(tile, location, angle, resize)
    self.m.shader.tile = tile
    self.m:draw()
end

SpriteSheetShader = {
vertexShader = [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

uniform highp vec2 tiles;
uniform highp vec2 tile;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

const float c_one = 1.0;

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    
    vTexCoord.x = texCoord.x / tiles.x + (c_one / tiles.x * tile.x);
    vTexCoord.y = texCoord.y / tiles.y + (c_one / tiles.y * tile.y);

    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]],
fragmentShader = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

void main()
{

    lowp vec4 col = texture2D( texture, vTexCoord  ) * vColor;

    //Set the output color to the texture color
    gl_FragColor = col;
}
]]
}

I’m curious. Is c_one /tiles.x * tile.x really better than tile.x/tiles.x?

Lol, that would be me running through adding stuff bit by bit and not refactoring the math. I started off with it referencing the sprite locations numbered from 1 and the maths had a bunch of extra junk in them, then I thought zero based even though not so lua ish was nicer…

@spacemonkey I see! I wondered if there was some sneaky shader optimisation going on there.

No, the only intentional optimisation is use constants rather than literal numbers. And that’s just if they appear loads. In GLSL if I have 1.0 in it 3 times it effectively creates 3 constants in the uniforms memory space for the shader, using a constant means it will only have 1.

Didn’t know that. Useful tip, thanks.

Hello. About constants in lua: this is half off-topic but you might both know the answer. Here is the context. Some month ago i had a discussion about defining constants in lua, as in C Const, which was not possible, so we we had to define variables, local for efficiency: (1) local MYOPTION = 1.
I have learned that lua never modify strings, but creates a new one each time. Moreover when we write var= "sample" it checks if the required string already exists, and if so it attributes the pointer adress of “sample” to the variable var. To check if var1==var it simply compares the adress of the 2 variables => the comparison of two strings is extremely fast.
So, my question: if all this is true, that would mean that at compilation time, the expression (2) MYOPTION = "1" is a pointer to a predefined adress, so maybe if the compiler finds if var1 == MYOPTION , it writes directly the string “1” adress in the compiled code, instead of variable key MYOPTION? It is possible, because the lua compiler seems very optimized. If it is true, that would mean that expression (2) is exactly like the Const declaration we were looking for.
What do you think? Does sby knows the bottom line?

Can anyone comment? Please? @Simeon?

I’m not really sure of this stuff in the lua engine of Codea, but I would suggest that the performance difference in general in worrying about this in lua is probably marginal unless you have a very specialised problem.

In shaders it’s a little different however because they have substantially restricted resources available. Even saying that the const optimisation I suggest is more of a good habit rather than something that will make or break your program. Specifically in the graphics hardware there is a memory space for uniform information, the minimum that has to be there from the OpenGL ES 2.0 spec is 128 vec4 entries. I think iPad is at this minimum. So that effectively gives you 512 floats to play with. (a vec3 uses 3 floats, a matrix uses 16 etc).

That memory is used by uniform variables, constant variables, literal values (ie if you write 1.0 in code) and implementation specific stuff. So littering your code with literals just burns this limited memory space unnecessarily.

Thank you for your answer @Spacemonkey.

Take out the background draw() and add a timer wrapper to the peopleTiles loop - I get 60 fps without the background and only 15 with. However, zelda characters moving at 60 fps are a blur :slight_smile:

.@aciolino, that is not too surprising, the background is drawing 24x24 tiles or 576 images per frame. The people is drawing 100 images per frame. The demo was attempting to push the thing hard to see how it performed, so it was 15fps doing 676 images per frame, individually translated in lua and blitted from a spritesheet in the shader.

for a stress test, not too bad :slight_smile: