In the last gamejam I participated (Ludumdare #23), I used the sweet and oh-so-sexy Axel Library! It’s by far my favorite Stage3D 2-dimensional library at this point. If you’re a fan of Flixel, then you will love Axel too.
There’s one thing I been wondering about during my gigantic quest to understanding how these libraries works. How easy (or difficult) it would be to create animated UVs for certain tiles in a tilemap?
By animated UVs, all I mean is simply: “being able to scroll the texture in the X-axis and Y-axis for a given tile. ”. So what are some of the effects that could be achieved? You could make:
- Bubbles Underwater;
- Racetrack speed-boost zones;
- Scrolling Background;
- Indicate the Flow of a stream, traffic lane, etc;
And as an extra challenge, I wanted to make multiple scrollable tiles possible by using only one single texture resource – to keep the overall texture memory low and swapping between textures to a minimum.
So here is the Texture I am using for demonstration purpose:
And here is a wacky example of what could be done with it in a scrollable Tilemap:
This area requires
Adobe FlashPlayer version 9.0.0 or above.
iOS Devices are not currently supported.
Move your mouse around to vary the scroll speed on the X-axis and Y-axis.
I’m still in the process of carrying this idea over to the Axel community, so for now… I’m mostly going to share my thought process with you rather than simply dropping all the unfinished source-code files in one big ZIP file for you to read over the weekend. Sounds good?
When I was thinking about a way to offset the UVs of a Tile (or quad), first thing that came up to mind was… the obvious “Oh, let’s just change the VertexBuffer’s data on-the-fly with the updated UV coordinates”. Now, this approach could of been enough, and fairly easy to put together – but it’s shitty in performance! Uploading new vertex-data in the VertexBuffer3D resource on EVERY frame is very bad practice (it practically defeats the purpose of using the GPU). Also, that method would mean you would be using ONE texture resource per animated tiles. Again, very bad for performance, and poor use of GPU resources. If you would have more content to draw (foreground, game elements, HUD, etc.), you would have to change your textures much more often in one single render-pass (due to the higher draw-count).
Ok, *yoink!* next plan…
So how about… we put all the tiles we need on a tilesheet / spritesheet, and try to animate certain tile’s UV offsets? Okay! This is getting better… buuuuuuut… Can’t do that either! Why? Because – as you scroll through the UVs of the texture in your tilemap, it will reveal any existing neighbouring tiles around it, found on the spritesheet. However, if you are planning only to scroll in one direction… well… you could make your spritesheet one column wide for horizontal scrolling, or one row high for vertical scrolling, and then you could just turn the “<wrap>” flag on for your Fragment Shader’s texture sampling.
With that being said… if we’re to make a custom shader to handle our UV Scrolling texture, we might as well do it right! Do it once and for all so that no matter where it is located in the spritesheet, the tile can be scrollable horizontally, vertically, or both!
Pointing to dynamically indexed Vertex-Constants
Although that may not be the correct term, but in AGAL, a dynamically indexed Vertex-Constants is basically the ability to use one register’s field value (a vertex-attribute’s field, like va0.x) to retrieve a certain vertex-constant by its index #. It is basically the same idea as accessing elements in an Array (ex: myArray), only in AGAL you would write:
//Move a vertex-constant's values (XYZW) to a temporary register: mov vt0, vc[va1.x];
Let’s say va1.x is equal 4 in this first instance. The temporary register (vt0) will be transferred the values of vc4 (vc[va1.x] resolves to vc)!
So you see, this is a great way to refer to a “group” or to centralize certain pieces of information for certain pieces of geometry. In the context of a game, all “water” tiles could have their UV offsets stored in Vertex-Constant #4, all “lava” tiles could have theirs associated to Vertex-Constant #6.
Is that all that’s required? Oh no! There’s more!
Restricting sampling of UVs within the bounds of the Tile
To restrict the texture sampling to a given tile, we must define it’s UV Start X, Start Y, Width and Height. In doing so, we’re specifying that:
- If a texture is sampled in its middle, the UV is originally sampled at X: 0.5, Y: 0.5, and then offsetted by X: 0.7, Y: 0.7, you would have a total of X: 1.2, Y: 1.2. This means it would be sampled out-of-bounds for your given tile! Totally undesirable ! To wrap the value, you must use the “frc” opcode (short for “fraction”) which just retains the numbers following the decimal point, in this case X: 0.2, Y: 0.2.
- Because UVs are generally from 0.0 to 1.0, one tile sprite might be defined as a rectangle at X:0, Y:0, Width:0.25, Height:0.25. As you can see, we’re dealing with small numbers here. So we will have to multiply them to a larger factor (only temporarily) so we can “capture” the fractional portion above 1.0 when the texture is sampled out-of-bounds. Once we have the fractional portion, we can divide it back by the same larger factor OR, multiply it by the inverse of the large factor used (personally I favor multiplication over division).
- Once we have our offseted value, it’s just a matter of sampling our texture with it, and then potentially perform additional color / alpha transformation with Fragment Shaders (if needed);
Here is the shader that I’ve put together to handle this:
public static const SHADER_VERTEX:Array = [ "mov v0, va1;", //Pass the uvs. "mov vt0, vc[va2.y];", //Store temporarly the Start X&Y and Width & Height "mov v1, vc[va2.x];", //Pass the uvOffset value to a variant. "mov v1.z, vt0.x;", //Pass the X of Start (origin) "mov v1.w, vt0.y;", //Pass the Y of Start (origin) "mov v2.xy, vt0.zw;", //Pass the Width & Height "rcp v2.z, vt0.z;", //Pass the reciprocals of Width "rcp v2.w, vt0.w;", //Pass the reciprocals of Height "m44 op, va0, vc0;" //Set the output of our vertex-shader (first and foremost!) ]; public static const SHADER_FRAGMENT:Array = [ //v0 = uvs //v1 = uvOffset.x + .y AND Tile's start.x + .y //v2 = width & height AND 1/width & 1/height "alias ft0, temp;", "alias v0.xy, uvOriginal;", "alias v1.xy, uvOffset;", "alias v1.zw, uvStart;", "alias v2.xy, dimensionsWH;", "alias v2.zw, reciprocalsWH;", "mul temp, uvOriginal, dimensionsWH;", //convert the 0-1 range to 0-[width of tile] range "add temp.xy, temp.xy, uvOffset;", //add the offset x&y "mul temp.xy, temp.xy, reciprocalsWH;", //multiply to larger number "frc temp.xy, temp.xy;", //only keep the fraction of the large number "mul temp.xy, temp.xy, dimensionsWH;", //multiply to smaller number "add temp.xy, temp.xy, uvStart;", //Add the Start X & Y of the tile "tex oc, temp, fs0 <2d,nearest,nomip>;" //Sample it! ];
How I use this shader probably is worth an explanation.
There is two VertexBuffers required:
- 1) to hold the X, Y, U, V values of each vertices;
- 2) to hold the two vertex-constant IDs necessary, usually N and N+1 (ex: 4 and 5)
Because I’m already using a Camera matrix in Vertex-Constant #0, it takes up the first 4 registers (0 to 3), so I start off my vertex-constant IDs at 4.
So how many vertex-constant IDs will you end up using? Totally depends up to you! You could reuse the same Vertex-Constant for all static (non-scrolling) tiles, or you could have all Tile types scrolling independently (but each individual tiles won’t necessarily scroll individually, although… you could set that up too). Because of the 128 vertex-constants limitation, I’m guesstimating that 62 tile-types ( 62 = (128 – 4[the camera matrix ]) / 2 ) would be possible to scroll independently. Still! Wow… that’s plenty!
When you do a rendering pass, you basically have to allocate your 1st VertexBuffer as two vertex-attributes (va0 and va1) of FLOAT_2 (first two values are X and Y, next two values are U and V), the 2nd VertexBuffer allocated as the third vertex-attribute (va2), and then make any necessary modifications to the Vector.<Number> objects passed in the Vertex-Constants allocated as the UV-offsets of your tiles. You will also need to pass Vector.<Number> objects to supply the Start X, Start Y, Width and Height of each tile-types. For example, Vertex-Constant #4 might contain UV offset X: 0.025, Y: 0.075, and the bounds of the tile could go in Vertex-Constant #5 as X: 0, Y: 0.5, Width: .25, Height: .25. So all of the Odd-Numbered constants would generally remain the same per frame, unless you were doing actual “tile animations ” as opposed to “tile scrolling ”.
Do note: if you apply this technique to a project that doesn’t require to draw any other geometry or rendering passes, you may skip updating your Odd-Numbered Vertex-Constants as those will generally not change. In fact, anything that will stay the same can just be initialized / allocated once at the start of your application.
So there you have it! A lengthy explanation on how to create scrollable tiles with UV offsets!
If you need more details, just let me know in the comments below :)
Happy AGAL‘ing and Stage3D‘ing!