Making a Tilemap Shader

Excellent. Works perfectly. Onward!

  • Caleb

Hm. After I save the PNG data, Preview gives me a corrupted file error. Opening it with ImageOptim and compressing it fixes it. Is there some way to fix this?

[EDIT] Maybe it’s just Preview. Affinity is opening it fine. So will OpenGL mind?

[EDIT AGAIN] Trying to load the image with a composite paint fill gives me  <Error>: ImageIO: PNG IHDR: CRC error. What to do?
 

[EDIT YET AGAIN] Using your Lua bit library makes the same error only it won’t even open in ImageOptim.

  • Caleb

Uff, an emotional rollercoaster reading those edits.  :mellow:

Sanity check : all four bitwise operators belong to the respective library, in either case?

I thought I had run this through bit , but I must be mistaken. I do remember being able to reopen the files I did make, though. Hmm. In the situations where they “worked”, did they look right, once converted?

It sounds like some of those programs might simply be more forgiving of a bad CRC. I suspect the

 c = .5 \* (c - bit)

line will also flake out when using bit , in cases where c looks negative (which will be pretty frequent, I’d imagine, given all the bxor ()'ing). See if

local bit = band(c, 0x1) c = rshift(c, 1) if bit ~= 0 then

works any better.

In my original application I had some fairly normal-sized images and so this whole process could take some time, to the point where I actually eked out performance gains by forgoing function calls where possible. Unfortunately, trouble like this comes along for the ride.  :frowning:

OpenGL won’t ever actually play a part in this. libpng is probably somewhere in the middle, or something like it. I have some image processing stuff I’d like to revisit, so I’m actually considering wrapping both that and some flavor of libjpeg into a plugin, for saving and loading purposes. I don’t know how soon that might be, though. Others are more than welcome to beat me to the punch!

Apologies for this roadblock, which I hope isn’t dampening your enthusiasm for the end goal!

!!!

Boy do I feel dumb. I figured out the problem - I had Tiled exporting in a compressed format. It’s all working fine now. It did give a corrupted error even with the correct export format, but changing the c = line fixed it.

Carrying on again!

  • Caleb

Sorry for the delay, I’ve been pretty busy lately.

The actual writing of the shader was much easier than I expected. On the first day (the 21st) I managed to complete an initial tilemap shader. I’ve named it Nightshade (see what I did there?), and, though it’s currently kind of hacked into Dusk’s structure, it works! As soon as I clean things up, I’ll push a new branch to the repo and you shader wizards can give any optimization suggestions.

  • Caleb

Heh, it’s always the “dumb” things that get you. All too common has been the case where I find myself running into some bizarre roadblock just as I seem to be tying the last loose ends on a module, when suddenly everything goes wrong. After much futile searching, I worry either that I’ve made some tragic design decision along the way, or that I’m in over my head with the material. And then (I can think of at least three such occasions in the fairly recent past) it turns out, nope, I just had two objects reusing the same table and stomping over one another’s data, with hilarious results. *sigh*

Sounds good on the shader. I know whenever I finish the above-mentioned image processing stuff, something like this would be great. In that case, the “entry sampler” is a hash function based on the position, rather than a texture, but the rest of the process should be the same. (This is where the uber-shader concept rears its head!)

The easiest way to do this is to create a new layer containing the color areas, then apply a modified version of the effect to function as a “mask” shader (it uses the layer texture to modify the background). It’s quite easy to do.

Hm. I thought up a really great idea to expand Corona’s vertex data capacity by storing multiple variables in one using bit ops (oh how I hate jumping through hoops!), but it seems that Corona’s version of GLSL ES doesn’t support them. And it seems that I can’t use a #version directive because Corona must pin on some code to the front of the shader. Is there a way to support bitwise ops in a shader at all?

[EDIT] #extension GL_EXT_gpu_shader4 : enable makes the shader compile, but is this a good idea? Like will Corona keep this supported?

  • Caleb

Hmm, were you actually able to feed anything in successfully?  :) Looks like just a desktop define, though?

Extensions are sort of hit-and-miss, depending as they do upon the hardware capabilities, driver, etc. Bitwise operators come standard in GLES 3.0, so newer devices ought to have all the machinery available, but it will probably be some time still before Corona migrates to that.

What I do in-shader is use this logic to pry apart an encoded float into two 10-bit numbers. I encode those in Lua using this routine (built atop this stuff), but I’m still not certain the num = … line is exact.[1] (A few things in that file need wider testing.) I think with some rewriting it might also be possible to eke out 11-bit numbers (2048 x 2048) given the GLES-guaranteed minima, but I haven’t tried.

After that you only have a couple more guaranteed bits, so I highly doubt 12-bit numbers will “Just Work”. That said, if such large ranges will be used for texture coordinates, it would be a weird thing to find a device that supports >= 1024-pixel dimensions but doesn’t provide more bits. Unfortunately the available ranges aren’t easily accessible just yet.

If you know the numbers will be 8-bit or less, there’s this older alternative. I’m not sure where the encoding function is, honestly. :) I could explain the idea behind it, though.

That said, for an effect like this, uniform userdata sound like they’ll be a good fit, whenever the interface is finally documented. I’m not sure if that stuff is still unstable or somebody just needs a little nudge to write it up.

[1] - I do have a test in mind.

Basically, a texture with 1024 x 1024 unique values, compared against as many 1-pixel rects, each sampled at the appropriate coordinate. The test would fail if any mismatch was found. Not quite as insane as it sounds, since everything’s static and so a few pixels at a time could be checked. Still, it sounds like a pain to write up.  :stuck_out_tongue:

Oh, bother. It’s just a desktop solution. On iPad, it says the extension is unavailable :(. The desktop version works perfectly, though, with bit magic and everything. It’s maddening - the shader works, I just need a way to pass more variables to it!

Due to GLSL’s 32-bit-float minimum, I was able to set the bit shift distance to 16 and it seems to be working fine. The distance can be tweaked in settings.lua, though, if you think that’s a bad idea. In that case, why would using 16 bits not be so good, if we know that a float is at minimum 32?

The current version is published as another branch in the GitHub repository, so check it out and see what you think.

  • Caleb

Hi. Sorry for the late reply.

It’s actually great to know that’s a viable option on desktop! I don’t particularly like my crazy solution, but it is what it is.  :D I feel your pain on the data limit, especially with such low-frequency effects. The great advantage of vertex userdata is that, as the name suggests, the inputs must be passed along as part of the vertex, in a spare attribute. I assume the stock Corona “master effect” employs one attribute each for position, color, and vertex userdata. (I guess that does bring to mind that you could smuggle in data through setFillColor (), if it’s not otherwise being used…) This allows subsequent uses of the same shader to be batched, which is far cheaper than doing tons of draw calls, but in this case that effectively happens anyway, so going through uniforms would be a wash.

For what it’s worth, there could very well be GLES extensions that offer the same. Here’s the list, if you care to do some digging: Khronos OpenGL ES Registry Typically these provide some symbols that can be #ifdef’d / #if defined(X) in your shader. They’ll also usually provide some friendly name so that on the Lua side you can do something like

local extensions\_list = {} for name in string.gmatch(system.getInfo("GL\_EXTENSIONS"), "[\_%w]+") do extensions\_list[name] = true end local is\_thing\_supported = extensions\_list["THING"] -- available?

Desktop GLSL might provide 32 bits (although I have my doubts about some integrated laptop GPUs), in which case it’s well and good, but mobile is not often so generous. 24-bit seems to be the minimum one can assume (thus some of my numbers, above). While GLES 3.0-focused, I found this to be a good analysis. Then again, if bit ops aren’t an option anyhow, not a big deal.

I’ll take a look at your code and see if I can incorporate the technique into Nightshade. I do like the idea of using setFillColor to get extra inputs. The problem is, the CoronaColorScale function (macro?) is all we get. I don’t even know where the color variables are stored.

What about multiplying and dividing by 2^n? Would that be efficient enough to be a viable alternative to real bitwise operators?

  • Caleb

Wow… I have to say, this whole thread is going way over my head :slight_smile:

Caleb, can you sort of summarize this in laymen’s terms? It sound to me like you have succeeded in building a tilemap by copying RGBA as a shader, right? I’m not following what else is needed, actually, because that sounds like it allows everything you’d want? Like I said, I’m not following well because it’s becoming too technical for my brain!

  1. Tiled stores the tiles a layer displays in the form of a long list of numbers, corresponding to each tile’s GID in a tileset.

  2. Dusk reads the tile numbers and generates an image based on them. Each tile GID is encoded in the image as a single pixel, with the pixel’s red value denoting the GID. A GID of 0 corresponds to a completely black pixel, and GIDs from then up make the pixels redder and redder. In other words, Dusk stores the layer’s tile data in an image. Dusk stores it in an image because GLSL can’t use a list of Lua numbers, but it can use pixels from a texture.

  3. To build Nightshade layers, Dusk creates a rectangle the size of the layer and tells it to shade itself using the Nightshade shader. The Nightshade shader takes two texture inputs: the data image created in step 2 and the tileset image the layer uses.

  4. Each time the shader colors a pixel in the rectangle, it finds the tile coordinate the pixel would fall into (e.g. for a 32x32 tileset, pixels 1 to 32 in X- and Y-axes would all be in tile coordinate [1, 1]), reads the red value at that location from the data image, and finds the tile in the tileset corresponding to the red-value-encoded GID (the image storage mechanism makes red values from 0-255, so a red value of 1 means tile 1 in the tileset, 2 means tile 2, etc.). Then it gets the correct pixel from that tile and tells the graphics engine to color the pixel in the layer rectangle that color.

As for all the bit-shifting trouble, that’s because Corona only allows developers to send 4 custom inputs to a shader. And how many do we need to render a tile layer? Width of the layer in pixels, height of the layer in pixels (because GLSL sends coordinates in the range of [0,1]), tileset width in tiles, tileset height in tiles, width of each tile in pixels, height of each tile in pixels, and, preferably, margin and spacing of the tileset. That makes at least 6 inputs required, and ideally 8. We can store multiple values in a single number by moving the bits over and using a 32-bit number (for desktop GLSL) as two 16-bit numbers, thereby giving us 8 inputs, but bit manipulation isn’t available in GLSL ES 2.0, which is what Corona uses. Thus, the bit-shifting alternative discussions.

  • Caleb

Ah yes… Okay, I see. Thanks for the superclear update!

To add to Caleb’s excellent summary, the difference in my technique for packing two values is that it encodes an integer[1] using exactly representable floating point values (refer to some of those links I mention above), then decodes them on the shader side accordingly.

Unfortunately, I’m stuck with what GLES 2.0 gives me, so I’m still unsure whether I have an exact decoding or just a good approximation;  thus the mention of a test. I’m at least heartened that it no longer wildly freaks out on some values.  :D  (To give an analogy, think about trying different identities for, say, 1 - cos(x). Mathematically they’re all equal, but in real-world computing, some might give more accurate / stable results than others. In this case I’m aiming for an equation that is 100% exact.)

[1] - Up to 20 bits, so  for example two integers from 0 to 1023, but able to just sneak in 1024 as well on a lowest-common-denominator device. Often these will only temporarily be integers, e.g. having undergone a math.floor(x * 1024) transformation, which will be undone once in the shader.

Wow, it’s been a while. I’ve decided that, if uniform userdata is coming fairly soon, I’ll just wait and go with that for clarity and “normal Corona-ness.” I posted a topic about it here: https://forums.coronalabs.com/topic/59706-uniform-userdata/

  • Caleb

If you were willing to trade-off flexibility, might you require that the tileset scheme be one of a few supported configurations, then just pass a single number to describe it?  (getting a 4-for-1 deal, otherwise you’d fall back to conventional tile rendering for “atypical” tileset schemes)

for example,

tileset scheme “1” might decode to:  256x256 sheet, 16x16 tiles

tileset scheme “2” might decode to:  512x512 sheet, 32x32 tiles

tileset scheme “3” might decode to:  512x512 sheet, 30x30 tiles, border 1 for aa edge extrude

etc

Hm. Interesting idea. I’ll think through it. Maybe a better way would be to encode only certain values as presets. That is, the layer width and height could be normal numbers, but the tile size could be presets, 'cause they’re usually 16x16/32x32/64x64. Or, alternatively, I could assume people use square tiles and store tile size in one variable. Good stuff to think about; thanks for bringing this approach up.

  • Caleb