Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello, shaders!

Crash-tan, saluting with a wink: purple hair, an orange hoodie with her name on it, and teal trainers.

It’s me, Crash-tan, again! This book is about one small corner of me: my shader language. A shader is a little program that decides what color every pixel of a layer, a sprite or a shape ends up, and it runs on the graphics chip, for all the pixels at once.

I’ll assume you’ve met my engine in Making Games with Crash, so a SCREEN, a SET SPRITE and a !! modifier are old friends. Everything around the shaders in here is Crash BASIC 2. The shaders themselves are written in a language of their own, and that language is what this book is about.

How this page works

Same as the engine’s book: the lesson is on this side, and on the other side there’s an editor and the game’s screen, with my whole engine running in your browser. Every piece of code has a Try it button. Press it, then change the code and press Run (or Ctrl + Enter) to see what your change does. With shaders, changing the numbers is most of the fun.

SCREEN 320, 240
SHADER "rainbow"
    WHEN DRAWING
        SET COLOR = RGBA(UV.x, UV.y, 1.0 - UV.x, 1.0)
    END WHEN
END SHADER

SET LAYER 0 !! shader("rainbow")
PAUSE

That’s a whole shader. For every pixel of layer 0, it says: be as red as you are far across, as green as you are far down, and as blue as you are close to the left. The screen fills with a smooth wash of color, and nobody drew a single pixel of it.

Crash-tan’s Tip: Every example in this book runs, here and in my players, and my tests check every one of them: the program runs, and the shader it makes is one a graphics chip accepts. If one ever goes wrong for you, that’s my bug, not yours.

What a shader can and can’t do

A shader is small on purpose. It gets one pixel: where it is, what color it was going to be, how long the game has been going, and the settings you give it. It hands back one color. That’s all.

It can’t see your program’s variables, it can’t remember anything from one pixel to the next or from one frame to the next, and it can’t draw anywhere but the pixel it’s on. What it gets in exchange is speed: a 320 by 240 screen is 76,800 pixels, and the graphics chip runs the shader on all of them sixty times a second without your game noticing.

What’s in this book

  • The language: the SHADER block, colors, where a pixel is, numbers and vectors, making choices without IF, time, settings, pictures and randomness.
  • Putting shaders on things: layers, sprites and shapes, shaders that a behavior runs, and transitions of your own for CLS.
  • A cookbook of shaders worth stealing.
  • The rules: the names you can’t use, the limits, and what my errors mean.
  • The reference: every word of the language on one page.

Let’s start with the block itself.

The SHADER block

Every shader is a SHADER block with a name. Inside it go the settings it takes, each on a USE line, and then one WHEN DRAWING block: the code that runs for every pixel.

SCREEN 320, 240
SHADER "darker"
    USE NUMBER amount DEFAULT 0.5
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * amount, c.g * amount, c.b * amount, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("darker")
PAUSE

Here’s what each part does:

  • SHADER "darker" names it. The name is how a layer, a sprite or a behavior asks for it, and defining a second shader with the same name replaces the first.
  • USE NUMBER amount DEFAULT 0.5 is a setting. Your program can change it while the game runs, and DEFAULT is what it is until it does. The next chapter but four is all about settings.
  • WHEN DRAWING … END WHEN is the code. It runs once for every pixel.
  • IMAGE is the color the pixel was going to be: what’s drawn on the layer there, or the sprite’s picture.
  • SET COLOR = is the color it is instead.

SET LAYER 0 !! shader("darker") puts the shader on layer 0, and from then on everything on that layer goes through it, including what’s drawn later.

Only two kinds of line

Inside WHEN DRAWING there are exactly two kinds of line. A name and = makes or changes a variable of the shader’s own:

c = IMAGE
grey = (c.r + c.g + c.b) / 3.0

And SET COLOR = says what the pixel’s color is:

SET COLOR = RGBA(grey, grey, grey, c.a)

There’s no IF, no loops, no PRINT, no FUNCTION. That sounds like a lot to give up, and it isn’t: the chapter on choices shows how a shader decides things without them.

The lines run top to bottom, and a variable can be given a new value as you go. If there’s more than one SET COLOR, the last one wins:

SCREEN 320, 240
SHADER "last"
    WHEN DRAWING
        c = IMAGE
        c = c * 0.5
        SET COLOR = RGBA(1.0, 0.0, 0.0, 1.0)
        SET COLOR = RGBA(c.r, c.g, c.b, 1.0)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("last")
PAUSE

The city comes out at half brightness, not red.

And a shader with no SET COLOR at all leaves every pixel as it was. That’s not useful by itself, but it’s a handy way to switch a shader off while you look for what’s wrong with it.

Comments

A comment in a shader starts with ', on a line of its own or at the end of one:

SCREEN 320, 240
SHADER "commented"
    ' Turn the picture upside down.
    WHEN DRAWING
        ' UV.y runs 0 at the top to 1 at the bottom, so this reads from the
        ' other end.
        SET COLOR = OWNTEXTURE(VEC2(UV.x, 1.0 - UV.y)) ' and that's all
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("commented")
PAUSE

The shader block is the one place in Crash BASIC 2 where // isn’t a comment. I hand the lines between SHADER and END SHADER to the shader language as you wrote them, and the shader language came from Crash BASIC 1, where a comment starts with '. So this stops, on the line with the //:

SCREEN 320, 240
SHADER "oops"
    WHEN DRAWING
        // Not a comment in here.
        SET COLOR = IMAGE
    END WHEN
END SHADER

Big and small letters

The shader language doesn’t mind how you write a word: SET COLOR, set color and Set Color are the same, and so are IMAGE, image and Image. So are your own variables: c and C are one variable.

SCREEN 320, 240
SHADER "casual"
    WHEN DRAWING
        Pixel = image
        set color = rgba(PIXEL.r, pixel.g, 0.0, 1.0)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("casual")
PAUSE

This book writes the language’s own words in capitals and your names in small letters, so you can tell them apart at a glance.

Colors

In a shader, a color is four numbers: red, green, blue and alpha, each from 0 to 1. Not 0 to 255 like a PSET, and not a palette number like a CIRCLE: 0 is none, 1 is all of it, and 0.5 is halfway. Alpha is how solid the color is, from 0, not there at all, to 1, solid.

RGBA(r, g, b, a) makes one, and RGB(r, g, b) makes one that’s solid.

SCREEN 320, 240
SHADER "orange"
    WHEN DRAWING
        SET COLOR = RGB(1.0, 0.5, 0.0)
    END WHEN
END SHADER

SET LAYER 0 !! shader("orange")
PAUSE

Taking a color apart

A color’s parts are .r, .g, .b and .a. Put a color in a variable and you can read them one by one:

SCREEN 320, 240
SHADER "swap"
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.b, c.r, c.g, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("swap")
PAUSE

The red goes where the green was, the green where the blue was, and the blue where the red was. The purple city turns gold.

You can take more than one part at once, in any order, and the same part more than once: c.bgr is the color’s blue, green and red, in that order, and c.rrr is three of its red. That’s called a swizzle. Three parts make a color with no alpha, so give it one back before SET COLOR sees it:

SCREEN 320, 240
SHADER "reds"
    WHEN DRAWING
        c = IMAGE
        r = c.rrr
        SET COLOR = RGBA(r.r, r.g, r.b, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("reds")
PAUSE

The city in black and white, by how red each pixel was. Four parts are a whole color, so SET COLOR = IMAGE.bgra is a color too.

Sums with colors

A color times a number makes every part of it bigger or smaller at once, alpha too. A color times a color multiplies part by part, which is how you tint something: a pixel times RGB(1.0, 0.6, 0.6) keeps all its red and loses some of its green and blue.

SCREEN 320, 240
SHADER "rosy"
    WHEN DRAWING
        SET COLOR = IMAGE * RGB(1.0, 0.6, 0.6)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("rosy")
PAUSE

+ and - work part by part too, so c + RGBA(0.2, 0.2, 0.2, 0.0) lightens a color, and RGBA(1.0, 1.0, 1.0, 1.0) - c is its opposite. Careful with that one: alpha gets turned around along with the rest, and a solid pixel becomes see-through. The cookbook’s invert keeps c.a as it was.

A part can go above 1 or below 0 in the middle of a sum, and that’s fine. The screen shows anything over 1 as 1 and anything under 0 as 0, and CLAMP (next chapter but one) holds a number inside a range when you’d like to keep it there yourself.

From one color to another

MIX(a, b, amount) is the color part of the way from a to b: all a at 0, all b at 1, and halfway between at 0.5. It’s the one function you’ll use more than any other.

SCREEN 320, 240
SHADER "fog"
    USE NUMBER thick DEFAULT 0.5
    WHEN DRAWING
        c = IMAGE
        SET COLOR = MIX(c, RGBA(0.7, 0.7, 0.8, c.a), thick)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("fog")
PAUSE

The city half lost in grey fog. The fog’s alpha is c.a, the pixel’s own, so the fog is exactly as solid as what it covers.

Crash-tan’s Tip: Keep c.a in mind on sprites. A sprite is a rectangle, and the parts of it around the character are pixels with an alpha of 0. A shader that hands back a solid color for those pixels paints the whole rectangle. Keeping the pixel’s own alpha keeps the shape.

Grey

How bright a color looks is mostly its green, a good deal of its red and a little of its blue. Adding them up in those amounts gives its grey:

SCREEN 320, 240
SHADER "grey"
    WHEN DRAWING
        c = IMAGE
        g = c.r * 0.3 + c.g * 0.59 + c.b * 0.11
        SET COLOR = RGBA(g, g, g, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("grey")
PAUSE

The plain average, (c.r + c.g + c.b) / 3.0, works too, but the blues come out too bright and the greens too dark next to how your eye sees them.

Where the pixel is

A shader runs for one pixel at a time, and the first thing it usually wants to know is which one.

UV

UV is where the pixel is on what the shader is drawing: (0, 0) at its top left, (1, 1) at its bottom right, and (0.5, 0.5) in the middle. UV.x is how far across, UV.y how far down. On a layer that’s across the layer; on a sprite it’s across the sprite, however big it is on screen.

SCREEN 320, 240
SHADER "stripes"
    WHEN DRAWING
        c = IMAGE
        band = FLOOR(UV.y * 8.0) / 8.0
        SET COLOR = RGBA(c.r + band * 0.5, c.g, c.b, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("stripes")
PAUSE

UV.y * 8.0 runs from 0 at the top to 8 at the bottom, and FLOOR drops what comes after the point, so band steps from 0 to 7/8 in eight bands, getting redder on the way down.

The letters UV are just what graphics people call these, after the U and V axes of a picture, to tell them apart from x and y on the screen.

Crash-tan’s Tip: On an animated sprite, UV runs from 0 to 1 across the frame it’s showing, not across the whole sheet. A shader means the same on frame 7 as on frame 0.

Reading the picture somewhere else

IMAGE is the color right under the pixel. OWNTEXTURE(uv) is the color anywhere else on the same picture: give it a place, as a UV, and it reads the picture there. That’s how a shader moves things around, because a pixel that shows what’s to its left makes the picture look moved to the right.

SCREEN 320, 240
SHADER "mirror"
    WHEN DRAWING
        SET COLOR = OWNTEXTURE(VEC2(1.0 - UV.x, UV.y))
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("mirror")
PAUSE

VEC2(x, y) makes a place out of two numbers, and 1.0 - UV.x is the same distance from the right as the pixel is from the left, so the layer comes out back to front.

A place doesn’t have to be made out of parts. UV is one already, and sums work on a whole place at once: UV * 2.0 doubles both its parts, and UV + VEC2(0.1, 0.0) is a little to the right.

SCREEN 320, 240
SHADER "zoom"
    USE NUMBER zoom DEFAULT 2.0
    WHEN DRAWING
        middle = VEC2(0.5, 0.5)
        SET COLOR = OWNTEXTURE((UV - middle) / zoom + middle)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("zoom")
PAUSE

Each pixel reads from half as far from the middle as it is, so the middle of the layer is blown up to twice the size.

Crash-tan’s Tip: Reading outside the picture, below 0 or above 1, gets you the nearest pixel on its edge, stretched out. If you’d rather it wrapped around, FRACT keeps only what comes after the point: `OWNTEXTURE(FRACT(UV

  • 3.0))` tiles the picture three times each way.

Pixels

UV is in fractions of the picture, and sometimes you want pixels: one pixel to the left, say, however big the picture is. PX(x, y) is how far x pixels across and y pixels down is, as a UV:

SCREEN 320, 240
SHADER "emboss"
    WHEN DRAWING
        c = IMAGE
        before = OWNTEXTURE(UV - PX(1.0, 1.0))
        d = (c.r + c.g + c.b) - (before.r + before.g + before.b)
        g = 0.5 + d
        SET COLOR = RGBA(g, g, g, 1.0)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("emboss")
PAUSE

Each pixel compares itself with the one up and to its left. Where they’re the same, it’s grey; where it’s brighter than its neighbour, lighter; where it’s darker, darker. The city comes out pressed into grey metal.

The pixels PX counts are the picture’s own: the layer’s on a layer, and the sprite’s frame on a sprite, before it’s scaled. So a one-pixel outline is one of the sprite’s pixels thick, even on a sprite drawn four times its size.

The screen

SCREEN is where the pixel is on the screen, in the game’s pixels, from (0, 0) at the top left. It’s for effects that belong to the screen rather than to what’s drawn: lines that stay where they are while a sprite moves through them.

SCREEN 320, 240
SHADER "lines"
    WHEN DRAWING
        c = IMAGE
        dark = STEP(0.5, FRACT(SCREEN.y / 4.0)) * 0.4
        SET COLOR = RGBA(c.r * (1.0 - dark), c.g * (1.0 - dark), c.b * (1.0 - dark), c.a)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((4, 4)) shader("lines")
    !! velocity((0.5, 0))
SHOW SPRITE 0
PAUSE

The hero walks through stripes that stay put on the screen, two of the game’s pixels light and two dark.

SCREEN_SIZE is the size SCREEN gave the game: 320 by 240 here. SCREEN counts the same pixels, so SCREEN / SCREEN_SIZE goes from 0 at the top left to 1 at the bottom right of the game, however big the window is, and the black bars of a letterbox aren’t part of it.

SCREEN 320, 240
SHADER "dusk"
    WHEN DRAWING
        c = IMAGE
        down = SCREEN.y / SCREEN_SIZE.y
        SET COLOR = MIX(c, RGB(0.9, 0.4, 0.1), down * 0.6)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("dusk")
PAUSE

The city goes orange towards the bottom of the screen, the same in a small window as in a big one.

Numbers and vectors

Everything in a shader is a number, or a few numbers kept together. A few numbers together are a vector: a place like UV is two, a color is four.

Made byHow manyIts parts
a number, like 0.51
VEC2(x, y), UV, SCREEN, SCREEN_SIZE, PX(x, y)2.x .y
VEC3(x, y, z), a swizzle of three3.x .y .z
VEC4(x, y, z, w), RGBA(...), RGB(...), IMAGE, OWNTEXTURE(...), SAMPLE(...)4.x .y .z .w

The parts of any vector can be called either way: .r .g .b .a are the same parts as .x .y .z .w, so UV.r is UV.x. A color reads better with .r, and a place with .x.

Every number is a number with a point: 1 and 1.0 are the same, and 7 / 2 is 3.5.

Sums

+, -, * and / work as they do in Crash BASIC: * and / before + and -, left to right otherwise, and brackets first. A minus in front of something turns it around.

Between two vectors of the same size, a sum works part by part. Between a vector and a number, the number goes into every part: UV * 2.0 is VEC2(UV.x * 2.0, UV.y * 2.0). Two vectors of different sizes can’t be added up, and that’s one of the ways a shader goes wrong:

SCREEN 320, 240
SHADER "mismatch"
    WHEN DRAWING
        SET COLOR = IMAGE + UV
    END WHEN
END SHADER

SET LAYER 0 !! shader("mismatch")

Functions

These work on a number, or on every part of a vector at once:

FunctionGives
ABS(x)x without its minus
FLOOR(x)x with what comes after the point dropped, downwards: FLOOR(-0.5) is -1
FRACT(x)only what comes after the point: x - FLOOR(x)
SIN(x), COS(x)sine and cosine, with x in radians
SQRT(x)the square root
POW(x, y)x to the power y
MIN(a, b), MAX(a, b)the smaller, the bigger
CLAMP(x, low, high)x, kept between low and high
MIX(a, b, t)the way from a to b: a at 0, b at 1
STEP(edge, x)0 if x is under edge, 1 if it’s not
SMOOTHSTEP(low, high, x)0 under low, 1 over high, and a smooth curve between

When a function takes more than one value, they all have to be the same size: two numbers, or two colors, but not a color and a number. MIN(c, 0.5) won’t do; MIN(c, RGBA(0.5, 0.5, 0.5, 0.5)) will. The one exception is MIX, whose last value, how far along, can be a number whatever the first two are.

SCREEN 320, 240
SHADER "capped"
    WHEN DRAWING
        SET COLOR = MIN(IMAGE, 0.5)
    END WHEN
END SHADER

SET LAYER 0 !! shader("capped")

And these take vectors and give a number back:

FunctionGives
LENGTH(v)how long v is: for a place, how far it is from (0, 0)
DOT(a, b)the parts multiplied in pairs and added up
NORMALIZE(v)v made 1 long: only its direction left. Not a number: a vector the same size

LENGTH is the one you’ll use most, for how far a pixel is from somewhere:

SCREEN 320, 240
SHADER "spotlight"
    USE NUMBER size DEFAULT 0.35
    WHEN DRAWING
        c = IMAGE
        far = LENGTH(UV - VEC2(0.5, 0.5))
        light = 1.0 - CLAMP(far / size, 0.0, 1.0)
        SET COLOR = RGBA(c.r * light, c.g * light, c.b * light, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("spotlight")
PAUSE

UV - VEC2(0.5, 0.5) is the way from the middle to the pixel, and its length is how far that is. Divided by size, it’s 1 at the edge of the light, and 1.0 - CLAMP(...) turns that into light: 1 in the middle, 0 at the edge and beyond.

Crash-tan’s Tip: The layer is wider than it is tall, and UV doesn’t know: it goes 0 to 1 both ways. So the “circle” above is an oval, as wide as the layer is. To make it round, stretch UV.x by how much wider the screen is than tall: VEC2((UV.x - 0.5) * SCREEN_SIZE.x / SCREEN_SIZE.y, UV.y - 0.5).

A swizzle on a sum

A swizzle goes on a name, on one of the language’s own words like UV, or on something in brackets: c.r, UV.x, (UV * 2.0).x. It can’t go straight on a function, so OWNTEXTURE(UV).r won’t do. Put the color in a variable first:

c = OWNTEXTURE(UV)
red = c.r

Choices without IF

A shader has no IF, and no <, >, AND or OR either. It makes its choices with sums instead, and once you’ve seen how, you’ll find it’s a neat way to think.

STEP: a choice as a number

STEP(edge, x) is 0 when x is under edge and 1 when it isn’t. That 0 or 1 is the choice, and multiplying by it is acting on it: something times 0 is gone, and something times 1 is all there.

SCREEN 320, 240
SHADER "half"
    WHEN DRAWING
        c = IMAGE
        right = STEP(0.5, UV.x)
        g = (c.r + c.g + c.b) / 3.0
        grey = RGBA(g, g, g, c.a)
        SET COLOR = MIX(c, grey, right)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("half")
PAUSE

right is 1 on the right half of the layer and 0 on the left, and MIX(c, grey, right) is c where it’s 0 and grey where it’s 1. The city is in color on the left and grey on the right.

Here’s how the things IF would say come out as sums, with each choice a 0 or a 1:

What you meanAs a sum
1 if x is edge or moreSTEP(edge, x)
1 if x is under edge1.0 - STEP(edge, x)
a if choice, otherwise bMIX(b, a, choice)
choice1 and choice2choice1 * choice2
choice1 or choice2MAX(choice1, choice2)
not choice1.0 - choice
1 if x is between low and highSTEP(low, x) * (1.0 - STEP(high, x))

That last one draws a band. Here’s a box, as one band across and one down:

SCREEN 320, 240
SHADER "box"
    WHEN DRAWING
        c = IMAGE
        across = STEP(0.25, UV.x) * (1.0 - STEP(0.75, UV.x))
        down = STEP(0.25, UV.y) * (1.0 - STEP(0.75, UV.y))
        inside = across * down
        dim = RGBA(c.r * 0.3, c.g * 0.3, c.b * 0.3, c.a)
        SET COLOR = MIX(dim, c, inside)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("box")
PAUSE

Everything outside the box is dimmed to 30%.

Crash-tan’s Tip: This isn’t slower than an IF would be. A graphics chip runs a whole group of pixels in step, so when some of them would take an IF one way and some the other, it does both for all of them and keeps the answer that’s wanted. Sums like these are what it was going to do anyway.

SMOOTHSTEP: a soft edge

STEP makes a sharp edge, and a sharp edge on a screen has jagged steps along it. SMOOTHSTEP(low, high, x) makes a soft one: 0 up to low, 1 from high on, and an easy curve between. The gap between low and high is how soft the edge is.

SCREEN 320, 240
SHADER "vignette"
    USE NUMBER soft DEFAULT 0.3
    WHEN DRAWING
        c = IMAGE
        far = LENGTH(UV - VEC2(0.5, 0.5))
        dark = SMOOTHSTEP(0.7 - soft, 0.7, far)
        SET COLOR = MIX(c, RGBA(0.0, 0.0, 0.0, c.a), dark)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("vignette")
PAUSE

The corners fade to black, the way an old camera’s do. Try soft at 0.01 for a hard edge, and at 0.6 for a gentle one.

Choosing between colors by brightness

A choice doesn’t have to be about where the pixel is. Here’s one about what it is: every pixel becomes one of three colors, by how bright it is.

SCREEN 320, 240
SHADER "poster"
    WHEN DRAWING
        c = IMAGE
        g = c.r * 0.3 + c.g * 0.59 + c.b * 0.11
        mid = STEP(0.15, g)
        high = STEP(0.35, g)
        dark = RGB(0.1, 0.05, 0.2)
        middle = RGB(0.9, 0.3, 0.4)
        light = RGB(1.0, 0.9, 0.5)
        SET COLOR = MIX(MIX(dark, middle, mid), light, high)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("poster")
PAUSE

The inner MIX picks dark or middle; the outer one replaces that with light when the pixel’s bright enough. Two choices, nested: the same as an IF … ELSEIF … ELSE.

Time

A shader doesn’t remember anything from one frame to the next, so it can’t count frames itself. I count them for it, two ways.

TIME

TIME is how many seconds my renderer has been running, with a point: 2.5 is two and a half seconds. It’s the same for every shader and every pixel, and it’s real time, so a shader that moves by TIME moves at the same speed on a slow phone and on a fast PC.

SIN turns time into something that goes back and forth: SIN(TIME) goes from 0 up to 1, down to -1 and back again every 6.28 seconds. Multiply TIME to make it faster.

SCREEN 320, 240
SHADER "pulse"
    USE NUMBER speed DEFAULT 3.0
    WHEN DRAWING
        c = IMAGE
        glow = 0.5 + SIN(TIME * speed) * 0.5
        SET COLOR = MIX(c, RGBA(1.0, 1.0, 1.0, c.a), glow * 0.6)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3)) shader("pulse")
SHOW SPRITE 0
PAUSE

0.5 + SIN(...) * 0.5 moves the back and forth from -1 to 1 up into 0 to 1, which is the range most things want.

TIME with FRACT goes round instead: FRACT(TIME) climbs from 0 to 1 every second and starts again. Here it scrolls the picture forever:

SCREEN 320, 240
SHADER "conveyor"
    USE NUMBER speed DEFAULT 0.25
    WHEN DRAWING
        SET COLOR = OWNTEXTURE(VEC2(FRACT(UV.x + TIME * speed), UV.y))
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (0, 0), "city"
SET LAYER 0 !! shader("conveyor")
PAUSE

TICK

TICK counts frames, from 0, since this shader went on this layer or this sprite. Where TIME is the same everywhere, TICK is each one’s own: put the shader on a sprite now and its TICK starts at 0 now, whatever any other sprite’s is doing. That makes it the one for things that happen once, starting when the shader goes on:

SCREEN 320, 240
SHADER "appear"
    USE NUMBER frames DEFAULT 60
    WHEN DRAWING
        c = IMAGE
        shown = CLAMP(TICK / frames, 0.0, 1.0)
        SET COLOR = RGBA(c.r, c.g, c.b, c.a * shown)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3)) shader("appear")
SHOW SPRITE 0
PAUSE

The hero fades in over 60 frames, a second at sixty frames a second.

Crash-tan’s Tip: TICK counts the frames that are drawn, so on a machine that’s struggling it counts more slowly than sixty a second, and the fade takes longer. It always finishes, though. TIME is never late, but it’s the renderer’s clock, not the shader’s: a one-off driven by TIME starts wherever the clock happens to be.

A one-off from a setting

When a one-off has to keep to real time, have your program tell the shader how far along it is. Here my program counts the ticks, and the shader only draws what it’s told:

SCREEN 320, 240
SHADER "wipe"
    USE NUMBER done DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        gone = STEP(UV.x, done)
        SET COLOR = MIX(c, RGBA(0.0, 0.0, 0.0, 1.0), gone)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("wipe") shader_params({done: 0})
done = 0
DO
    IF COOLDOWN(1) AND done < 1
        done = done + 1 / 90
        SET LAYER 0 !! shader_params({done: done})
    END IF
LOOP

The city is wiped away from the left, in 90 ticks. The next chapter is all about settings like done.

Settings

A shader can’t see your program’s variables. What it needs from your game comes in through its settings: the USE lines at the top of the block.

USE NUMBER

USE NUMBER name is a number your program can set. DEFAULT says what it is until your program does; without one, it’s 0.

SCREEN 320, 240
SHADER "tint"
    USE NUMBER red DEFAULT 1.0
    USE NUMBER green DEFAULT 1.0
    USE NUMBER blue DEFAULT 1.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * red, c.g * green, c.b * blue, c.a)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(60) y(100) scale((3, 3))
    !! shader("tint")
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("hero") x(180) y(100) scale((3, 3))
    !! shader("tint") shader_params({green: 0.4, blue: 0.4})
SHOW SPRITE 1
PAUSE

shader_params goes with shader, and takes a dictionary of names and values. The first hero has the defaults, all 1, and looks as it always does. The second has less green and blue, and comes out red. Two sprites, one shader, a setting each: a setting belongs to the layer or sprite the shader is on, not to the shader.

A setting you don’t give keeps its default. A setting the shader doesn’t have is a mistake, and I stop and tell you which ones it does have:

SCREEN 320, 240
SHADER "tint"
    USE NUMBER red DEFAULT 1.0
    USE NUMBER green DEFAULT 1.0
    USE NUMBER blue DEFAULT 1.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * red, c.g * green, c.b * blue, c.a)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) shader("tint") shader_params({gren: 0.4})

There’s no gren, so I stop, and say the settings are blue, green and red. The same goes for a shader name I don’t know: shader("tnt") stops too.

Changing a setting

On a layer, shader_params by itself, without shader, changes the settings of the shader that’s already on, and leaves the rest of them as they were:

SCREEN 320, 240
SHADER "tint"
    USE NUMBER red DEFAULT 1.0
    USE NUMBER green DEFAULT 1.0
    USE NUMBER blue DEFAULT 1.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * red, c.g * green, c.b * blue, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("tint")
t = 0
DO
    IF COOLDOWN(1)
        t = t + 0.02
        SET LAYER 0 !! shader_params({red: 0.6 + SIN(t) * 0.4, green: 0.6 + SIN(t + 2) * 0.4})
    END IF
LOOP

blue is never changed, so it stays at its default, and the red and green drift in and out of step with each other. Changing a layer’s numbers this way is cheap, so it’s fine to do every tick.

It changes the shader that’s on, so there has to be one. On a layer with no shader, there’s nothing for the numbers to go to, and I stop:

SCREEN 320, 240
SET LAYER 0 !! shader_params({red: 0.5})

A sprite’s or a shape’s settings go with its shader every time: give shader again, with the new shader_params.

SCREEN 320, 240
SHADER "tint"
    USE NUMBER red DEFAULT 1.0
    USE NUMBER green DEFAULT 1.0
    USE NUMBER blue DEFAULT 1.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * red, c.g * green, c.b * blue, c.a)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
SHOW SPRITE 0
hurt = FALSE
DO
    IF COOLDOWN(30)
        hurt = NOT hurt
        IF hurt
            SET SPRITE 0 !! shader("tint") shader_params({green: 0.3, blue: 0.3})
        ELSE
            SET SPRITE 0 !! shader("tint")
        END IF
    END IF
LOOP

The hero goes red, then back, every half second. Without shader_params, the settings go back to their defaults.

Crash-tan’s Tip: For a setting on a sprite that changes every tick, a glow or a flash, let a behavior run the shader: it moves the setting for you, without your program sending the shader again each time. That’s in the chapter on behaviors.

USE COLOR

USE COLOR name is a color your program can set: four numbers, red, green, blue and alpha, each from 0 to 1, as a shader sees them. Its DEFAULT is written RGB(r, g, b) or RGBA(r, g, b, a); without one, it’s black. In the shader, it’s a color like any other, and .r, .g, .b and .a are its parts.

Your program gives it as three numbers, or four with the alpha, in brackets like a place:

SCREEN 320, 240
SHADER "tint"
    USE COLOR tint DEFAULT RGB(1.0, 1.0, 1.0)
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * tint.r, c.g * tint.g, c.b * tint.b, c.a)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(40) y(100) scale((3, 3))
    !! shader("tint") shader_params({tint: (1.0, 0.4, 0.4)})
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("hero") x(140) y(100) scale((3, 3))
    !! shader("tint") shader_params({tint: (0.4, 1.0, 0.4)})
SHOW SPRITE 1
ENABLE SPRITE 2
SET SPRITE 2 !! image("hero") x(240) y(100) scale((3, 3))
    !! shader("tint") shader_params({tint: [0.4, 0.4, 1.0]})
SHOW SPRITE 2
PAUSE

A red hero, a green one and a blue one. The last color is a list, which works just as well as brackets. Give three numbers and the alpha is 1; 255 in a palette is 1 here.

A color takes the room of four numbers, so a shader can have up to 16 of them, or fewer and some USE NUMBERs besides.

USE IMAGE

USE IMAGE name is a picture the shader can read from, besides its own. SAMPLE(name, uv) reads it, the way OWNTEXTURE(uv) reads the shader’s own picture, and your program says which picture it is by giving its name as the setting:

SCREEN 320, 240
SHADER "window"
    USE IMAGE outside
    WHEN DRAWING
        c = IMAGE
        view = SAMPLE(outside, UV)
        bright = c.r * 0.3 + c.g * 0.59 + c.b * 0.11
        SET COLOR = MIX(view, c, STEP(0.2, bright))
    END WHEN
END SHADER

LOADIMG "city.png", "city"
LOADIMG "greyscale.png", "tiles"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("window") shader_params({outside: "tiles"})
PAUSE

Wherever the city is dark, you see the tiles picture through it instead.

A picture is stretched over the whole of what the shader’s on: UV runs 0 to 1 across it whatever size it is. A shader can use up to four pictures.

Crash-tan’s Tip: A picture is given when the shader goes on, with shader and shader_params together, on a layer as on a sprite. To change which picture it reads, put the shader on again with the new one.

What a DEFAULT can be

A DEFAULT is a plain number, like 0.5 or 3. It can’t be a sum, and it can’t have a minus in front. When a setting should start out below zero, give it its value from your program, with shader_params, as the shader goes on.

Randomness

A shader has no RND that’s different every time: the same pixel has to come out the same every frame, or the screen would boil. What it has instead is RND(x, y): a number from 0 to 1 that looks random, made from x and y. The same x and y always give the same number, and numbers next to each other give numbers that have nothing to do with each other.

SCREEN 320, 240
SHADER "snow"
    WHEN DRAWING
        pixel = FLOOR(UV * SCREEN_SIZE)
        n = RND(pixel.x, pixel.y)
        SET COLOR = RGB(n, n, n)
    END WHEN
END SHADER

SET LAYER 0 !! shader("snow")
PAUSE

An old television between stations, holding perfectly still: a random grey for each of the game’s pixels. FLOOR(UV * SCREEN_SIZE) is which pixel this is, counted in the game’s pixels, so every bit of the window that shows that pixel gets the same number. RND(x) with one number works too, for a random number along a line.

Making it move

Put TIME in, and it’s different every moment. FLOOR it first and it changes in steps, which is the noise of a broken television rather than a blur:

SCREEN 320, 240
SHADER "static"
    USE NUMBER rate DEFAULT 12
    WHEN DRAWING
        frame = FLOOR(TIME * rate)
        pixel = FLOOR(UV * SCREEN_SIZE)
        n = RND(pixel.x + frame * 7.0, pixel.y + frame * 13.0)
        SET COLOR = RGB(n, n, n)
    END WHEN
END SHADER

SET LAYER 0 !! shader("static")
PAUSE

Twelve new screens of noise a second.

Random blocks

Give RND the same numbers for a whole patch of pixels and the whole patch gets the same random number. FLOOR(UV * 10.0) is the same for every pixel in a tenth of the layer each way, so this picks a random color for each block:

SCREEN 320, 240
SHADER "blocks"
    WHEN DRAWING
        cell = FLOOR(UV * 10.0)
        r = RND(cell.x, cell.y)
        g = RND(cell.x + 50.0, cell.y)
        b = RND(cell.x, cell.y + 50.0)
        SET COLOR = RGB(r, g, b)
    END WHEN
END SHADER

SET LAYER 0 !! shader("blocks")
PAUSE

Adding 50 to one of the numbers gives an unrelated random number for the same block, so its red, green and blue don’t all come out the same.

Dissolving

Random numbers make good dissolves. Each pixel gets its own random number, and goes when the dissolve’s progress passes it:

SCREEN 320, 240
SHADER "dissolve"
    USE NUMBER gone DEFAULT 0.5
    WHEN DRAWING
        c = IMAGE
        n = RND(FLOOR(UV.x * 64.0), FLOOR(UV.y * 64.0))
        keep = STEP(gone, n)
        SET COLOR = RGBA(c.r, c.g, c.b, c.a * keep)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
    !! shader("dissolve") shader_params({gone: 0.5})
SHOW SPRITE 0
PAUSE

Half the hero’s pixels are gone. Turn gone from 0 up to 1 and the hero crumbles away.

Crash-tan’s Tip: RND is made of a SIN of a big number, which is as random as a shader can cheaply get. It’s for looks, not for dice: give it numbers in the thousands and it gets less random, because the graphics chip’s numbers have only so many digits. Keep what you give it small.

Layers, sprites and shapes

A shader goes on three kinds of thing, each with its own SET, and each with the same three modifiers: shader to put one on, shader_params for its settings, and clear_shader to take it off. (Only a layer takes shader_params by itself, to change the settings of a shader that’s already on; a sprite or a shape is given shader again.)

Layers

On a layer, a shader runs over the whole layer as it goes to the screen: everything drawn on it, and the empty parts too.

SCREEN 320, 240
SHADER "night"
    USE NUMBER dark DEFAULT 0.6
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * (1.0 - dark), c.g * (1.0 - dark), c.b, c.a)
    END WHEN
END SHADER

CREATE LAYER 1, 320, 240
LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 1
CIRCLE (160, 120), 40, 14, 14
SET LAYER 0 !! shader("night")
PAUSE

The city on layer 0 goes dark and blue, and the yellow circle on layer 1 doesn’t: a shader on a layer is for that layer only. Put the same shader on layer 1 as well and both go dark.

A shader on a layer doesn’t change what’s drawn on it, only how it looks. Take it off with SET LAYER 0 !! clear_shader and the city is as bright as it ever was.

Sprites

On a sprite, a shader runs over the sprite’s picture, at the size and place it’s drawn. Moving, turning and scaling the sprite move the shader with it.

SCREEN 320, 240
SHADER "outline"
    USE NUMBER red DEFAULT 1.0
    USE NUMBER green DEFAULT 1.0
    USE NUMBER blue DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        right = OWNTEXTURE(UV + PX(1.0, 0.0))
        left = OWNTEXTURE(UV - PX(1.0, 0.0))
        below = OWNTEXTURE(UV + PX(0.0, 1.0))
        above = OWNTEXTURE(UV - PX(0.0, 1.0))
        around = MAX(MAX(right.a, left.a), MAX(below.a, above.a))
        edge = around * (1.0 - c.a)
        SET COLOR = MIX(c, RGBA(red, green, blue, 1.0), edge)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3)) shader("outline")
    !! rotate(15)
SHOW SPRITE 0
PAUSE

A pixel that’s see-through itself, but has a solid pixel beside it, is on the outside edge of the hero, and gets painted yellow. The hero is tilted, and the outline tilts with it.

Three things to know about shaders on sprites:

  • Alpha. Whatever alpha your shader hands back, it’s multiplied by the sprite’s own. A sprite fading out with its shader on fades out with it, and your shader doesn’t have to know.
  • The edge. A shader only runs on the sprite’s own rectangle. An outline has to fit inside the picture, which is why the one above is drawn on the see-through pixels around the hero, not outside them. A picture with no see-through border has nowhere for an outline to go.
  • Frames. On an animated sprite, UV and PX are the frame’s, and OWNTEXTURE stays inside the frame: reading past its edge gets the edge, not the next frame along.

Shapes

A polygon takes a shader with SET POLYGON, the same way, as long as it has a picture: give it one with texture. IMAGE is the picture, and UV runs across the box around the shape, from its leftmost point to its rightmost and its top to its bottom.

SCREEN 320, 240
SHADER "sunset"
    WHEN DRAWING
        top = RGB(1.0, 0.8, 0.2)
        bottom = RGB(0.8, 0.1, 0.4)
        SET COLOR = MIX(top, bottom, UV.y)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
hill = mutation([(40, 200), (160, 60), (280, 200)])
SET POLYGON 0 !! bind(hill) texture("city") shader("sunset")
PAUSE

A triangle filled with a sunset, gold at its peak and pink at its foot. The city it was given is never seen, because this shader doesn’t read IMAGE; but without a picture, a shape is drawn in its fill color and the shader doesn’t run on it.

Taking a shader off

clear_shader takes it off, and a layer or sprite goes back to how it was.

SCREEN 320, 240
SHADER "upside"
    WHEN DRAWING
        SET COLOR = OWNTEXTURE(VEC2(UV.x, 1.0 - UV.y))
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3)) shader("upside")
SHOW SPRITE 0
upside = TRUE
DO
    IF COOLDOWN(60)
        upside = NOT upside
        IF upside
            SET SPRITE 0 !! shader("upside")
        ELSE
            SET SPRITE 0 !! clear_shader
        END IF
    END IF
LOOP

The hero stands on its head for a second, then the right way up for a second, and so on.

Shaders in behaviors

A behavior can run a shader. USE SHADER inside a BEHAVIOR puts the shader on any sprite that has the behavior, for as long as it has it.

SCREEN 320, 240
SHADER "stone"
    WHEN DRAWING
        c = IMAGE
        g = c.r * 0.3 + c.g * 0.59 + c.b * 0.11
        SET COLOR = RGBA(g, g * 0.95, g * 0.9, c.a)
    END WHEN
END SHADER

BEHAVIOR "petrified"
    USE SHADER "stone"
END BEHAVIOR

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
    !! behavior("petrified")
SHOW SPRITE 0
PAUSE

Turned to stone. That’s the same as !! shader("stone") on the sprite, but it travels with the behavior: a pattern that gives a sprite "petrified" gives it the look, too.

Moving a setting

What makes a behavior worth it is that it can move one of the shader’s settings for you, from one value to another over some ticks:

SCREEN 320, 240
SHADER "flash"
    USE NUMBER amount DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = MIX(c, RGBA(1.0, 1.0, 1.0, c.a), amount)
    END WHEN
END SHADER

BEHAVIOR "hit"
    USE SHADER "flash" !! param("amount") start(1) to(0) duration(20)
END BEHAVIOR

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
SHOW SPRITE 0
DO
    IF COOLDOWN(60)
        SET SPRITE 0 !! behavior("hit")
    END IF
LOOP

Once a second the hero is hit: it flashes white, and fades back to itself over 20 ticks.

  • param("amount") is the setting to move.
  • start(1) is where it starts, and to(0) where it ends.
  • duration(20) is how many ticks it takes.

Add !! loop and it goes there and back for as long as the sprite has the behavior, which is how a shield glows or a power-up that’s about to run out blinks:

SCREEN 320, 240
SHADER "flash"
    USE NUMBER amount DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = MIX(c, RGBA(0.4, 0.8, 1.0, c.a), amount)
    END WHEN
END SHADER

BEHAVIOR "shielded"
    USE SHADER "flash" !! param("amount") start(0) to(0.7) duration(30) loop
END BEHAVIOR

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
    !! behavior("shielded")
SHOW SPRITE 0
PAUSE

Crash-tan’s Tip: A behavior counts in ticks, and so the setting it moves keeps step with the game, not with the screen. A flash that takes 20 ticks takes a third of a second on any machine. That’s what to use when a shader effect has to line up with something in the game: a hit, a jump, a door opening.

Transitions of your own

CLS with !! effect clears a layer with one of my transitions: a fade, a melt, a dissolve. !! effect_shader clears it with a transition you wrote.

A transition is a shader that takes a setting called progress. I put it on the layer, move progress from 0 to 1, and when it gets there I take the shader off and clear the layer for real. All your shader has to do is draw the layer progress of the way to gone.

SCREEN 320, 240
SHADER "curtain"
    USE NUMBER progress DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        side = ABS(UV.x - 0.5) * 2.0
        closed = STEP(1.0 - progress, side)
        SET COLOR = MIX(c, RGB(0.5, 0.0, 0.1), closed)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
showing = FALSE
DO
    IF COOLDOWN(60) AND transition = ""
        IF showing
            CLS 0 !! effect_shader("curtain")
        ELSE
            PUTIMAGE (32, 0), "city"
        END IF
        showing = NOT showing
    END IF
LOOP

A curtain closes over the city from both sides, the city’s gone, and a second later it’s back to be closed on again.

side is 0 in the middle of the layer and 1 at both edges: how far out to the side the pixel is. As progress goes up, 1.0 - progress comes down from 1, and STEP closes the curtain over every pixel whose side it’s dropped under: the edges first, the middle last.

While it runs, transition is the shader’s name, as you gave it to CLS, and "" once it’s done:

SCREEN 320, 240
SHADER "curtain"
    USE NUMBER progress DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        side = ABS(UV.x - 0.5) * 2.0
        closed = STEP(1.0 - progress, side)
        SET COLOR = MIX(c, RGB(0.5, 0.0, 0.1), closed)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
CLS 0 !! effect_shader("curtain")
PRINT "now running:", transition
now running: curtain

The color it clears to

CLS takes the color to clear to, and the transition can know it: give the shader USE NUMBER settings called target_r, target_g, target_b and target_a, and I fill them in, from 0 to 1. A transition that draws the layer going to that color, rather than to a color of its own, ends on just what’s there when it finishes.

SCREEN 320, 240
SHADER "rise"
    USE NUMBER progress DEFAULT 0
    USE NUMBER target_r DEFAULT 0
    USE NUMBER target_g DEFAULT 0
    USE NUMBER target_b DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        risen = STEP(1.0 - progress, UV.y)
        SET COLOR = MIX(c, RGB(target_r, target_g, target_b), risen)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
CLS 1 !! effect_shader("rise")
PAUSE

Blue water rises over the city from the bottom, and the layer’s left blue: 1 is blue in the palette, and that’s what target_r, target_g and target_b were.

How fast

!! speed says how fast, the same as for my own transitions: 60, the speed it has if you don’t say, takes a second; 120 takes half a second; 30 takes two.

SCREEN 320, 240
SHADER "rise"
    USE NUMBER progress DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = MIX(c, RGB(0.0, 0.0, 0.0), STEP(1.0 - progress, UV.y))
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
CLS 0 !! effect_shader("rise") speed(20)
PAUSE

Three seconds of darkness rising.

Crash-tan’s Tip: A transition shader is an ordinary shader, so try it out as one before it’s a transition: put it on the layer with shader_params({progress: 0.5}) and look at it halfway, then at 0.1 and 0.9.

A cookbook

Shaders worth stealing. Each one is whole: put it in your game, give it to a layer or a sprite, and change the numbers until it’s yours.

Sepia

An old photograph: grey first, then tinted brown.

SCREEN 320, 240
SHADER "sepia"
    WHEN DRAWING
        c = IMAGE
        g = c.r * 0.3 + c.g * 0.59 + c.b * 0.11
        SET COLOR = RGBA(MIN(g * 1.2, 1.0), g, g * 0.8, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("sepia")
PAUSE

Negative

Every color turned to its opposite, and the alpha left alone so a sprite keeps its shape.

SCREEN 320, 240
SHADER "negative"
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("negative")
PAUSE

Pixelate

Big pixels: every pixel in a block reads the same spot, the block’s corner. Turn blocks down for bigger ones.

SCREEN 320, 240
SHADER "pixelate"
    USE NUMBER blocks DEFAULT 40
    WHEN DRAWING
        SET COLOR = OWNTEXTURE(FLOOR(UV * blocks) / blocks)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("pixelate")
PAUSE

Old monitor

Dark lines across the screen, and the red and blue each pulled a little to one side, the way a cheap old monitor smears them.

SCREEN 320, 240
SHADER "monitor"
    USE NUMBER split DEFAULT 1.0
    USE NUMBER lines DEFAULT 0.3
    WHEN DRAWING
        red = OWNTEXTURE(UV + PX(split, 0.0))
        green = IMAGE
        blue = OWNTEXTURE(UV - PX(split, 0.0))
        row = STEP(0.5, FRACT(UV.y * SCREEN_SIZE.y / 2.0))
        shade = 1.0 - lines * row
        SET COLOR = RGBA(red.r * shade, green.g * shade, blue.b * shade, green.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("monitor")
PAUSE

UV.y * SCREEN_SIZE.y is which row of the layer’s pixels this is, so every other pair of rows is darker, however big the window is.

Under water

Each row of the picture swings from side to side, a little later than the one above it.

SCREEN 320, 240
SHADER "underwater"
    USE NUMBER size DEFAULT 2.0
    USE NUMBER waves DEFAULT 20.0
    WHEN DRAWING
        swing = SIN(UV.y * waves + TIME * 3.0) * size
        c = OWNTEXTURE(UV + PX(swing, 0.0))
        SET COLOR = MIX(c, RGBA(0.0, 0.3, 0.6, c.a), 0.25)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("underwater")
PAUSE

size is in pixels, by way of PX, so it’s the same swing on any layer.

Heat haze

Like under water, but small and fast, and only near the bottom, over a fire or a hot road.

SCREEN 320, 240
SHADER "haze"
    WHEN DRAWING
        low = SMOOTHSTEP(0.5, 1.0, UV.y)
        swing = SIN(UV.y * 90.0 + TIME * 12.0) * 1.5 * low
        SET COLOR = OWNTEXTURE(UV + PX(swing, 0.0))
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("haze")
PAUSE

Hit flash

A sprite that’s hit goes white and fades back, from a behavior, so your program only has to say when.

SCREEN 320, 240
SHADER "white"
    USE NUMBER amount DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        SET COLOR = MIX(c, RGBA(1.0, 1.0, 1.0, c.a), amount)
    END WHEN
END SHADER

BEHAVIOR "hit"
    USE SHADER "white" !! param("amount") start(1) to(0) duration(12)
END BEHAVIOR

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3))
SHOW SPRITE 0
DO
    IF COOLDOWN(45)
        SET SPRITE 0 !! behavior("hit")
    END IF
LOOP

Burning away

A dissolve with a glowing edge: each pixel has a random number, goes when gone passes it, and glows orange just before it does.

SCREEN 320, 240
SHADER "burn"
    USE NUMBER gone DEFAULT 0
    WHEN DRAWING
        c = IMAGE
        n = RND(FLOOR(UV.x * 40.0), FLOOR(UV.y * 40.0))
        keep = STEP(gone, n)
        glow = keep * (1.0 - STEP(gone + 0.08, n))
        lit = MIX(c, RGBA(1.0, 0.5, 0.1, c.a), glow)
        SET COLOR = RGBA(lit.r, lit.g, lit.b, lit.a * keep)
    END WHEN
END SHADER

LOADIMG "hero.png", "hero"
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(140) y(100) scale((3, 3)) shader("burn")
SHOW SPRITE 0
gone = 0
DO
    IF COOLDOWN(1)
        gone = gone + 0.01
        IF gone > 1.1
            gone = 0
        END IF
        SET SPRITE 0 !! shader("burn") shader_params({gone: gone})
    END IF
LOOP

keep is whether the pixel’s still there, and glow is whether it’s still there but won’t be for much longer: there, and not past gone + 0.08 yet.

Glitch

Most of the time, nothing. Every so often, for a moment, rows of the picture jump sideways and the colors come apart.

SCREEN 320, 240
SHADER "glitch"
    USE NUMBER often DEFAULT 0.15
    WHEN DRAWING
        moment = FLOOR(TIME * 8.0)
        on = 1.0 - STEP(often, RND(moment))
        row = FLOOR(UV.y * 24.0)
        jump = (RND(row, moment) - 0.5) * 30.0 * on
        moved = UV + PX(jump, 0.0)
        red = OWNTEXTURE(moved + PX(4.0 * on, 0.0))
        rest = OWNTEXTURE(moved)
        SET COLOR = RGBA(red.r, rest.g, rest.b, rest.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("glitch")
PAUSE

Eight times a second, moment changes, and RND(moment) decides whether this eighth of a second glitches: on is 1 about 15% of the time. While it’s on, each of 24 bands of rows jumps by its own random amount, and the red is read from four pixels further along than the rest.

Rules and limits

The language is small, and so are its rules. Here they all are, in one place, so that when something stops you, you can find out why.

When things go wrong

A shader can go wrong in three places, and I tell you about all of them.

The first is when I read it. A word I don’t know, a bracket that isn’t closed, a <: I stop on the line it’s on, the way I would anywhere else in your program.

SCREEN 320, 240
SHADER "unknown"
    WHEN DRAWING
        SET COLOR = RGBA(TAN(UV.x), 0.0, 0.0, 1.0)
    END WHEN
END SHADER

TAN isn’t one of the shader language’s functions, so I stop at it.

The second is when I build it, as the SHADER line runs. A function given the wrong number of values, a name that was never set, a setting you try to change: I stop and say what it was.

SCREEN 320, 240
SHADER "short"
    WHEN DRAWING
        SET COLOR = RGBA(1.0, 0.0, 0.0)
    END WHEN
END SHADER

RGBA takes four values, and this one has three, so I stop and say so. (RGB is the one that takes three.)

SCREEN 320, 240
SHADER "early"
    WHEN DRAWING
        SET COLOR = RGBA(glow, 0.0, 0.0, 1.0)
        glow = FRACT(TIME)
    END WHEN
END SHADER

glow is used on the line before the one that sets it, so there’s no glow yet where it’s used.

The third is when the graphics chip builds it. What reads as fine to me can still be something the chip won’t run: a color with three parts where it wants four, a sum of a color and a place. Then I stop the game with the chip’s own words for what was wrong, and the shader’s name.

SCREEN 320, 240
SHADER "three"
    WHEN DRAWING
        SET COLOR = VEC3(1.0, 0.0, 0.0)
    END WHEN
END SHADER

SET COLOR wants four parts, and VEC3 has three.

Names

A variable’s name is letters, digits and _, starting with a letter. Big and small letters are the same.

These names are taken, and a variable called any of them stops the shader:

  • Names that start with one of the language’s own words. IMAGE, UV, SCREEN, SCREEN_SIZE, TIME and TICK are read first, so timer, uvs, images and ticks are read as TIME, UV, IMAGE or TICK with something left over. I stop on the line, and say which word the name starts with. Call them clock, place, pictures and count instead.
  • A setting’s name. A variable can’t have the same name as one of the shader’s USE settings: settings can be read, not changed.
  • The names I use to build the shader: uv, in, uniforms and balpha_out.
  • The graphics chip’s own words, such as var, let, fn, if, for, loop, return, struct, type, target, smooth, filter, true and false. The whole list is long, and mostly words nobody would use for a variable by accident. I know them all, and stop on any of them with the name you used.
SCREEN 320, 240
SHADER "clock"
    WHEN DRAWING
        timer = FRACT(TIME)
        SET COLOR = RGBA(timer, 0.0, 0.0, 1.0)
    END WHEN
END SHADER

What a line can be

  • name = expression, or SET COLOR = expression, and nothing else.
  • No IF, loops, FUNCTION, PRINT or DIM.
  • No <, >, = in a sum, AND, OR, NOT or MOD: see Choices without IF.
  • A number is digits with an optional point: 3, 0.25. No 1e-3.
  • A swizzle goes on a name, a word like UV, or brackets, not straight on a function: c = OWNTEXTURE(UV) then c.r.
  • A swizzle has one to four parts, all from rgba or all from xyzw, and only the parts the thing has: UV.z doesn’t exist.
  • You can’t change one part of a variable: c.r = 1.0 isn’t a line. Make a new color instead: c = RGBA(1.0, c.g, c.b, c.a).
  • USE lines come before WHEN DRAWING, and there’s one WHEN DRAWING.
SCREEN 320, 240
SHADER "swizzled"
    WHEN DRAWING
        red = OWNTEXTURE(UV).r
        SET COLOR = RGBA(red, 0.0, 0.0, 1.0)
    END WHEN
END SHADER

How many

WhatHow many
Numbers in the settings, a USE NUMBER one and a USE COLOR four64
USE IMAGE pictures4
Parts in a swizzle4

What I do for you

  • Alpha. On a sprite, the alpha your shader hands back is multiplied by the sprite’s own, so fading a sprite works with any shader on it.
  • Frames. On an animated sprite, UV, PX and OWNTEXTURE are the frame’s, not the sheet’s.
  • Edges. Reading a picture outside 0 to 1 gets you its edge.
  • Pixels, not blurs. Reading a picture between two of its pixels gets you the nearer one, not a blend. That’s what keeps pixel art crisp, and why a very slow scroll moves in steps.

Reference

The whole shader language, on one page.

The block

SHADER "name"
    USE NUMBER name [DEFAULT number]
    USE COLOR name [DEFAULT RGB(r, g, b)]
    USE IMAGE name
    WHEN DRAWING
        name = expression
        SET COLOR = expression
    END WHEN
END SHADER
LineMeans
SHADER "name" … END SHADERDefines a shader called name, or replaces the one already called that.
USE NUMBER name [DEFAULT n]A number setting. Without DEFAULT it’s 0. n is a plain number, with no minus.
USE COLOR name [DEFAULT RGBA(r, g, b, a)]A color setting, four numbers; RGB(r, g, b) for an opaque one. Without DEFAULT it’s opaque black. Given as three or four numbers from 0 to 1: (1.0, 0.5, 0.0) or a list.
USE IMAGE nameA picture setting, given by its name in shader_params, read with SAMPLE(name, uv). Up to 4.
WHEN DRAWING … END WHENThe code, run once for every pixel.
name = expressionMakes or changes a variable.
SET COLOR = expressionThe pixel’s color: four parts, 0 to 1. The last one wins; with none, the pixel keeps IMAGE.
' commentTo the end of the line.

What a pixel knows

WordPartsIs
IMAGE4The color the pixel was going to be.
UV2Where it is on what’s being drawn: (0, 0) top left, (1, 1) bottom right. On an animated sprite, on the frame.
SCREEN2Where it is on the screen, in the game’s pixels, from (0, 0) at the game’s top left.
SCREEN_SIZE2The game’s size, in its own pixels, as SCREEN set it.
TIME1Seconds since the renderer started.
TICK1Frames drawn since this shader went on this layer or sprite.

Functions

FunctionGives
RGBA(r, g, b, a)A color.
RGB(r, g, b)A solid color.
VEC2(x, y), VEC3(x, y, z), VEC4(x, y, z, w)A vector.
OWNTEXTURE(uv)The color of the shader’s own picture at uv.
SAMPLE(name, uv)The color of a USE IMAGE picture at uv.
PX(x, y)x pixels across and y down, as a UV, in the picture’s own pixels.
RND(x), RND(x, y)A number from 0 to 1 that looks random, the same every time for the same numbers.
ABS(x)x without its minus.
FLOOR(x)x rounded down to a whole number.
FRACT(x)What comes after the point: x - FLOOR(x).
SIN(x), COS(x)Sine and cosine, in radians.
SQRT(x)Square root.
POW(x, y)x to the power y.
MIN(a, b), MAX(a, b)The smaller, the bigger.
CLAMP(x, low, high)x, kept from low to high.
MIX(a, b, t)a at 0, b at 1, between in between.
STEP(edge, x)0 if x is under edge, otherwise 1.
SMOOTHSTEP(low, high, x)0 to 1, smoothly, as x goes from low to high.
LENGTH(v)How long a vector is.
DOT(a, b)Parts multiplied in pairs, added up.
NORMALIZE(v)v, 1 long.

Every function but RGBA, RGB, the VECs, OWNTEXTURE, SAMPLE, PX, RND, LENGTH and DOT works on a vector part by part, as well as on a number. When it takes more than one value, they’re all the same size, except the last of MIX, which can be a number.

Sums

+ - * /, with * and / first, brackets before both, and - in front to turn a number around. Vector and vector, of the same size, part by part; vector and number, the number in every part.

Swizzles

.r .g .b .a or .x .y .z .w, one to four of them, in any order, on a name, a word, or brackets: c.bgr, UV.yx, (UV * 2.0).x.

Putting a shader on

OnSettingsOff
A layerSET LAYER n !! shader("name")!! shader_params({...}), with shader or by itself!! clear_shader
A spriteSET SPRITE n !! shader("name")!! shader_params({...}), with shader!! clear_shader
A shape, with a textureSET POLYGON n !! shader("name")!! shader_params({...}), with shader!! clear_shader
A behaviorUSE SHADER "name"!! param("name") start(a) to(b) duration(ticks) [loop]when the behavior ends
A transitionCLS [color] !! effect_shader("name") [speed(n)]progress, target_r, target_g, target_b, target_a, filled inwhen it’s done

In shader_params, a number goes to a USE NUMBER setting, three or four numbers in brackets or a list to a USE COLOR one, and a picture’s name, as a string, to a USE IMAGE one. A name the shader doesn’t have, or a value of the wrong kind, stops the program and says which settings the shader takes.