210. Tiling images from a spritesheet
Warning: this post is a little advanced. Shader knowledge is desirable.
Suppose you’ve collected all the images you need for a project, and put them into a single image (a spritesheet)
- to make installation easier, and
- so you can include as many objects possible in a single mesh to speed up drawing (each mesh can only use one image)
It is a bit messy figuring out all the texture coordinates (as a fraction of 1) for each image in the spritesheet, eg if your spritesheet is 100 x 120 pixels, and you have included an image 30 x 50 whose bottom left pixel is at (0,40), then the texture coordinates of the top right corner are (30/100, (50+40)/120) = (0.3, 0.75).
But suppose you want to tile (ie repeat) your image over a large area. I’ve already shown how to do this here, using a shader, but now I only want to tile a small part of the (spritesheet) image. Help!
Tiling shader
What I did was to adapt my tiling shader to handle partial images. That shader only has one line that is different from the default shader provided with Codea, in the fragment shader.
--original line lowp vec4 col = texture2D(texture, vTexCoord); --line that does tiling lowp vec4 col = texture2D(texture, vec2(mod(vTexCoord.x,1.0),mod(vTexCoord.y,1.0)));
The original line simply uses the fractional texture coordinates assigned to this pixel,eg (0.3, 0.75), and looks this point up in the image attached to the mesh, to get the colour of this pixel.
The tiling modification allows the fractions to be greater than 1, so if you want to repeat your image three times sideways across a rectangular mesh, you set the corner texture coordinates as (0,0), (3,0), (3,1), (0,1). The shader will just use the fractional part of the coordinates, so it will repeat the texture three times.
In practice, I usually calculate the texture coordinates like this
local tw,td=meshWidth/img.width,meshHeight/img.height local t={vec2(0,0),vec2(tw,0),vec2(tw,td),vec2(0,td)} --four corners
and then I use these to create the coordinates for each vertex.
Scaling
If you use pictures from the internet, they will almost always be the wrong size. When tiling, you can easily allow for this. Suppose you want the image to be 10% of its original size. This means we will need 1/10% = 10x as many tiles, and we can allow for this in the calculations as shown below (where scale = proportion of original size, eg 10%).
local tw,td=meshWidth/img.width/scale,meshHeight/img.height/scale
Tiling partial images
Now we are ready to tackle the problem of tiling just a part of the mesh image.
I pass the shader a vec4 containing the (x,y) bottom left position of the image, as well as the width and height of the image I want to tile, eg (0.45, 0.36, 0.15, 0.08) . In my app, I’m not using colour variables for my mesh (because I’m using textures), so I put these values into the colour value for every mesh vertex. This means I have to multiply all those fractional values by 255.
What is great is that I don’t need to change anything about how I calculated the texture coordinates above, ie I can ignore the fact that my image is just a small part of a larger spritesheet, and base my coordinates on it, just as I would if it was a stand alone image. The “color” vector will do the job of telling the shader where the image is on the spritesheet.
The shader passes the “color” vector through to the fragment shader, where we can use the four values to add an offset to the normal tiling position, to position the shader correctly on the spritesheet.
lowp vec4 col = texture2D(texture, vec2(vColor.r+vColor.b*mod(vTexCoord.x,1.0),vColor.g+vColor.a*mod(vTexCoord.y,1.0)))*intensity;
So this is still just a one line change to the standard shader.
Note – if you need to use the color values in your shader, you can create a table of vec4 and pass it to the shader as a set of additional attributes, making sure to feed it from the vertex shader to the fragment shader.
Here is a simple code example. Slide the Scaling parameter to change the size of the tiled image, which will always fill the same space on the screen.
function setup() --create a spritesheet for our demo --just put one image in the middle somewhere img=image(500,600) setContext(img) sprite("Platformer Art:Block Brick",200,300) --70x70 setContext() --image scaling parameter.number("Scaling",0,1,1,function() CreateMesh() end) --create mesh m=CreateMesh() end function CreateMesh() --make a big stack of brown blocks local w,h=490,420 --vertices are calculated normally, size is multiple of 70 so it tiles evenly v11,v21,v22,v12=vec2(0,0),vec2(w,0),vec2(w,h),vec2(0,h) local v={v11,v21,v22,v22,v12,v11} --create table of vertices --texture coordinates --calculate number of tiles required local tw,th = w/70/Scaling, h/70/Scaling t11,t21,t22,t12=vec2(0,0),vec2(tw,0),vec2(tw,th),vec2(0,th) local t={t11,t21,t22,t22,t12,t11} --colour values are special --block image has bottom left corner at (200-35,300-35) = (165,265) --calculate as proportion of image size, then multiply by 255 --so we can use it as a colour value --also include width, height local col=color(165/500,265/600,70/500,70/600)*255 c={} for i=1,#v do c[i]=col end --create mesh m=mesh() m.texture=img m.vertices=v m.texCoords=t m.colors=c m.shader=shader(FracTileShader.v,FracTileShader.f) return m end function draw() background(50) translate(100,100) m:draw() end FracTileShader={ v=[[ uniform mat4 modelViewProjection; attribute vec4 position; attribute vec2 texCoord; attribute vec4 color; varying highp vec2 vTexCoord; varying lowp vec4 vColor; void main() { vTexCoord=texCoord; vColor=color; gl_Position = modelViewProjection * position; } ]], f=[[ precision highp float; uniform lowp sampler2D texture; varying highp vec2 vTexCoord; varying lowp vec4 vColor; void main() { lowp vec4 col = texture2D(texture, vec2(vColor.r+vColor.b*mod(vTexCoord.x,1.0),vColor.g+vColor.a*mod(vTexCoord.y,1.0))); gl_FragColor=col; } ]] }
Trackbacks & Pingbacks