My plan to post weekly devlog entries seems to have gone awry. Although I keep tracking my daily thoughts/activities, the process of collecting and organizing them is cumbersome. Also, since I’m still in a somewhat brainstorm phase of the projects, the amazing ideas I write one day rapidly become pure nonsense the day after. I will (hopefully) improve in the future.

For now, let’s move on.

In this entry I’d like to talk a bit about shaders.

Personally, I’ve been fascinated by shaders since my first encounter with them in the late nineties, when I installed a GPU-powered video-card in my Pentium computer. I liked them from the beginning because they carry along with them some of the magic and the charm of the old-school effects we were in awe of during the Commodore days (and I already missed my Amiga and C64 sooo much even then). The concept itself is resembles the idea of copper-list1: a sequence of instructions executed in parallel on a dedicate chip.

Fifteen years later or such, they are everywhere… and for a reason. With a handful of instructions one can easily implement some nice visual effects without heavily burdening the CPU.

Among the many, it seems that 2D water shaders are quite a bit popular. Almost everybody seems to be interested in developing and/or using them in their games/demos.

Me included, or at least that’s what I thought at first. I will, of course, implement and use a 2D water shader in the game, if it would be the chance (in fact, originally in this entry I wanted to present in detail an awesome fragment-shader implementing a cute bi-dimensional reflection effect). But I won’t be using fragment-shaders just to properly and nicely edulcorate the visual appeal of the game. Most of all, I’ll try and exploit them to simulate the kind of effects that the Copper did back in the days.

For example, to render copper-bars, that thanks to ShaderToy, you can see this simple shader in action directly here! ;)

For the curious ones, there the code snippet.

// Basic colors.
const vec4 BRIGHT_RED = vec4(1.0, 0.0, 0.0, 1.0);
const vec4 BRIGHT_YELLOW = vec4(1.0, 1.0, 0.0, 1.0);
const vec4 BRIGHT_GREEN = vec4(0.0, 1.0, 0.0, 1.0);
const vec4 BRIGHT_CYAN = vec4(0.0, 1.0, 1.0, 1.0);
const vec4 BRIGHT_BLUE = vec4(0.0, 0.0, 1.0, 1.0);
const vec4 BRIGHT_PURPLE = vec4(1.0, 0.0, 1.0, 1.0);
const vec4 RED = vec4(0.8, 0.0, 0.0, 1.0);
const vec4 YELLOW = vec4(0.8, 0.8, 0.0, 1.0);
const vec4 GREEN = vec4(0.0, 0.8, 0.0, 1.0);
const vec4 CYAN = vec4(0.0, 0.8, 0.8, 1.0);
const vec4 BLUE = vec4(0.0, 0.0, 0.8, 1.0);
const vec4 PURPLE = vec4(0.8, 0.0, 0.8, 1.0);
const vec4 DARK_RED = vec4(0.4, 0.0, 0.0, 1.0);
const vec4 DARK_YELLOW = vec4(0.4, 0.4, 0.0, 1.0);
const vec4 DARK_GREEN = vec4(0.0, 0.4, 0.0, 1.0);
const vec4 DARK_CYAN = vec4(0.0, 0.4, 0.4, 1.0);
const vec4 DARK_BLUE = vec4(0.0, 0.0, 0.4, 1.0);
const vec4 DARK_PURPLE = vec4(0.4, 0.0, 0.4, 1.0);
const vec4 WHITE = vec4(1.0, 1.0, 1.0, 1.0);
const vec4 BRIGHT_GREY = vec4(0.8, 0.8, 0.8, 1.0);
const vec4 DARK_GREY = vec4(0.4, 0.4, 0.4, 1.0);
const vec4 BLACK = vec4(0.0, 0.0, 0.0, 1.0);

// Shader parameters, you would probably define them as "uniform".
const float FREQUENCY_MULTIPLIER = 5.0;
const float AMPLITUDE_MULTIPLIER = 0.25;
const float BASE_POSITION = 0.5;
const float HEIGHT = 0.0500;

// Computes the copperbar colors for the [uv] pixel.
vec4 copperbar(in vec2 uv,
               in float base, in float offset, in float frequency, in float amplitude,
               in vec4 innerColor, in vec4 outerColor) {
    float alpha = (iGlobalTime + offset) * frequency; // Offset and scale current time.
    float position = base + (sin(alpha) * amplitude); // Get the copperbar middle position.
    float ratio = abs(uv.y - position) / HEIGHT; // Normalized (to height) distance.
    if (ratio > 1.0) { // Pixel is beyond copperbar limit, set to black.
        return BLACK;
    }
    return mix(innerColor, outerColor, ratio); // Mix to generate a gradient.
}

// Picks the first non-black color in the array.
vec4 choose(in vec4[5] colors) {
    for (int i = 0; i < colors.length(); ++i) {
        vec4 color = colors[i];
        if (color != BLACK) {
            return color;
        }
    }
    return BLACK;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;
    fragColor = choose(vec4[](
        copperbar(uv, BASE_POSITION + 0.0, 0.00, FREQUENCY_MULTIPLIER * 1.0, AMPLITUDE_MULTIPLIER * 1.0, BRIGHT_CYAN,    DARK_BLUE),
        copperbar(uv, BASE_POSITION + 0.0, 0.10, FREQUENCY_MULTIPLIER * 1.0, AMPLITUDE_MULTIPLIER * 1.0, BRIGHT_YELLOW, DARK_RED),
        copperbar(uv, BASE_POSITION + 0.0, 0.20, FREQUENCY_MULTIPLIER * 1.0, AMPLITUDE_MULTIPLIER * 1.0, BRIGHT_YELLOW, DARK_GREEN),
        copperbar(uv, BASE_POSITION + 0.0, 0.30, FREQUENCY_MULTIPLIER * 1.0, AMPLITUDE_MULTIPLIER * 1.0, PURPLE, DARK_YELLOW),
        copperbar(uv, BASE_POSITION + 0.0, 0.40, FREQUENCY_MULTIPLIER * 1.0, AMPLITUDE_MULTIPLIER * 1.0, BRIGHT_GREY, BLUE)
    ));
}

( See you next time! )


1 Thanks to Amiga memory-mapped I/O, the Copper is used to achieve a lot of interesting and clever effects. One typically exploit it to surpass the platform conventional limits (e.g. to increase the maximum amount of colors and sprites on screen). Coincidentally, also on the C64 a similar result can be achieved, albeit through a combination of busy-loop vertical synchronization and memory poking (which is what a copper-list does, in the end).