January 27, 2018

Shaders are Easy (Step 1: Spend Years Writing C Code)

Today I wrote my first shader! I'd never written code directly for the GPU before. It turns out to be pretty straightforward. Mostly because in high school I wrote a lot of C, and also nowadays I've been writing a lot lot of Objective-C (which you can pry from my cold dead hands, thankyouverymuch).

That said, I don't intend to intimidate you if you are interested in learning to write shaders. You don't need to have the same scars that I do. It's just that I was able to piggyback on my past experience.

Step 2 was I took an existing shader by one xaot88—which generates a moving star field—and bashed my head against it until it was malleable enough to do what I wanted. Incidentally, that's how I'd originally started out with programming. I learn best by getting my hands dirty. Anyway, that customized shader is now part of my upcoming game, Yomi the Witch. The first place it's used is in a progress bar that fills up as you build a streak of correctly-cast spells. See the bottom left of the adjacent screenshot.

This bar was previously a bland rectangle of a single color, fading from red to yellow to green based on how charged up your magic is. Much better this way, especially because it animates nicely, too.

The second place I use this shader is in the credits scene. I hadn't planned to use it anywhere besides the spell-power bar. However, while I was debugging the shader, I had it cover my entire phone screen to better diagnose issues. I thought it looked quite nice, especially with the iPhone X's display, so I had to use that effect for something. The credits seemed natural.

This first shader was me remixing someone else's work. The second one, an animated pinwheel shader, I wrote from scratch. Surprisingly, Shadertoy didn't already have something like this. There was one shader that was vaguely close, but I wanted the traditional two-color effect. Here's the code I ended up with:

void mainImage(out vec4 o, vec2 i)
{
    vec4 color1 = vec4(1, 1, 1, 1);
    vec4 color2 = vec4(0, 0, 0, 1);
    float speed = 2.0;
    float spokes = 12.0;
    vec2 anchorPoint = vec2(0.5, 0.5);

    vec2 uv = i / iResolution.xy;

    float theta = atan(uv.y - anchorPoint.y, uv.x - anchorPoint.x);
    float percent = theta / (2.0*3.14159);
    if (floor(mod(percent * spokes + speed*iTime, 2.0)) == 0.0) {
        o = color1;
    }
    else {
        o = color2;
    }
}

To explain briefly what's happening if you're unfamiliar with shaders: recall that the GPU executes this program once for each and every pixel, on each and every frame (of which there will hopefully be 60 per second). The function parameter i is the x, y coordinate of that pixel. After some variable declarations (to make it easy to customize this shader's effects), we create a variable uv which normalizes this pixel's coordinates into the unit square so x and y are both between 0 and 1. Then we find the angle, theta, between the pixel and the center point. We divide the angle by atan()'s range (in radians, so 2π) to again normalize to a fraction between 0 and 1, called percent. If at this point we were to render each pixel's percent as a grayscale color, we would see a round gradient except for a hard border between white and black. However, instead of a gradient from black to white, we want to see only pure black and white, alternating. We can achieve that with a combination of the modulus operator ("is this an even or an odd spoke?") and floor(). We mix the current timestamp, iTime into the operation to produce animation. Finally, we assign one of the two colors to the function parameter o which the display then uses to color that pixel.

Anyway, the reason I wanted to write this pinwheel shader was simple. When the player learns a brand new spell, it is important to really kick up the visuals to indicate how important and exciting it is. This, which is what I had up until this morning, is very much not exciting:

So having done the hard part of writing the shader, SpriteKit makes it very easy to plug it into the game. Edith immediately dubbed this the "Powerpuff Girls effect" which is right on.

And then, combining the pinwheel with a "focus" feature I already had, the resulting effect now looks like this:

I hope you'll agree, adding all this little polish really makes a big difference. Not bad for less than a day like five years of work. Though watching it now I need to clean up some of those animations… especially on fade out… rendering the pinwheel as a square is a bit too crude… all right, back to work!