Today I’ll talk about a shader idea I’ve always wanted to use but never got to release in a game until now. It has its roots in the old retro palette scrolling technique – or at least was inspired by it.

Palette scrolling was the thing when images had 8 bit pixels with each pixels being an index into a palette table of 256 RGB entries. That way using the same image and just changing the palette, one could change the look of the image without actually altering any pixels. Artists would do wonderful animations with just changing palettes. One of the cheapest way to do that would be to just shift the palette one entry (scroll it) and then see the colours shift – I called that palette scrolling.

These days one can still do palette scrolling but on current GPU hardware that involves using two textures: one index texture and one 1D palette texture. Animation being achieved by dynamically changing the palette texture. While on desktop GPU hardware that’s entirely fine on current mobile device GPUs dependent texture fetches are not very performance friendly.

I wanted to use a similar concept of having a static texture that would change appearance when “something like a palette” would change.

I do that by exposing a range from a gradient texture using a step function. For a quick refresher on the topic, have a look at this excellent post on step and pulse functions: http://realtimecollisiondetection.net/blog/?p=95

By using a gradient texture and a step function, y = sat(ax + b), I can vary the parameters and a and b and reveal/animate different parts of the said texture. I also introduce two colours and interpolate between then based on the y value.

Here is the shader code:

uniform mediump vec2 GradientParams; uniform lowp vec4 GradientColour0; uniform lowp vec4 GradientColour1; ... mediump vec4 col = texture2D(Texture, texVar); // calculate a*x + b mediump float y = col.x*GradientParams.x + GradientParams.y; // calculate sat(a*x + b) by clamping y = clamp(y, 0.0, 1.0); //sat (a*x + b) // interpolate the two colours based on the resulting y value lowp vec4 rcol = GradientColour0*(1.0 - y) + GradientColour1*y; // factor in the original texture alpha col.xyz = rcol.xyz; col.a *= rcol.a; //apply the variant colour gl_FragColor = col*colorVar;

The a and b parameters go in the GradientParameters x and y components and two colours at each extreme is respectively GradientColour0 (for y=0) and GradientColour1 (for y=1).

Let’s take a simple gradient texture:

And then apply our shader to it. We are using the function y=sat(ax+b). We use a=1 and b=0 thus giving us a gradient of 0 to 1 in the range of the texture. Then we are going to assign a colour at y=0 to be white (255,255,255) and at y=1 we’ll assign it to be black (0,0,0). Here’s how that would look.



Next let’s try to use a part of the range. We’ll use the same function but use a = 3.3 and b=-0.9. That way y will be 0 until x reaches 0.3 and then grow linearly to 1 until x reaches 0.6. To illustrate that I’ve assigned colours to be red for y=0 and blue for y=1.

Here’s one based on the same gradient texture that illustrates the way I use this effect. I assign a=2 and b=0 and that gives me a gradient between 0 and 0.5. I also assign the y=1 colour to be translucent – alpha=0.0. That way by varying the parameter a with some dynamic game value, I can get the bar to move with that value.

In MZR I link a lot of effects to the music EQ so that visuals appear to bounce with with music.

The above examples are using our simple horizontal gradient texture. Things get a bit more interesting as we start using more complicated textures. For example here’s the actual texture I use for my effect in MZR and the final result next to it. The green MZR logo in the texutre is to indicate where the start of the gradient is – it’s a grey gradient that fills up a maze.

And here’s the intro sequence to MZR where this effect is used:

The parameters are linked to the music EQ. The video shows the effect which is a single render call as well as the logo rendered on top and a FUNKY CIRCUIT sign underneath.

That’s it for now. See you next time.