Hello, shaders!
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
SHADERblock, colors, where a pixel is, numbers and vectors, making choices withoutIF, 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.5is a setting. Your program can change it while the game runs, andDEFAULTis what it is until it does. The next chapter but four is all about settings.WHEN DRAWING…END WHENis the code. It runs once for every pixel.IMAGEis 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.ain 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,
UVruns 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,
FRACTkeeps 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 by | How many | Its parts |
|---|---|---|
a number, like 0.5 | 1 | |
VEC2(x, y), UV, SCREEN, SCREEN_SIZE, PX(x, y) | 2 | .x .y |
VEC3(x, y, z), a swizzle of three | 3 | .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:
| Function | Gives |
|---|---|
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:
| Function | Gives |
|---|---|
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
UVdoesn’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, stretchUV.xby 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 mean | As a sum |
|---|---|
1 if x is edge or more | STEP(edge, x) |
1 if x is under edge | 1.0 - STEP(edge, x) |
a if choice, otherwise b | MIX(b, a, choice) |
choice1 and choice2 | choice1 * choice2 |
choice1 or choice2 | MAX(choice1, choice2) |
not choice | 1.0 - choice |
1 if x is between low and high | STEP(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
IFwould be. A graphics chip runs a whole group of pixels in step, so when some of them would take anIFone 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:
TICKcounts 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.TIMEis never late, but it’s the renderer’s clock, not the shader’s: a one-off driven byTIMEstarts 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
shaderandshader_paramstogether, 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:
RNDis made of aSINof 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,
UVandPXare the frame’s, andOWNTEXTUREstays 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, andto(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,TIMEandTICKare read first, sotimer,uvs,imagesandticksare read asTIME,UV,IMAGEorTICKwith something left over. I stop on the line, and say which word the name starts with. Call themclock,place,picturesandcountinstead. - A setting’s name. A variable can’t have the same name as one of the
shader’s
USEsettings: settings can be read, not changed. - The names I use to build the shader:
uv,in,uniformsandbalpha_out. - The graphics chip’s own words, such as
var,let,fn,if,for,loop,return,struct,type,target,smooth,filter,trueandfalse. 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, orSET COLOR = expression, and nothing else.- No
IF, loops,FUNCTION,PRINTorDIM. - No
<,>,=in a sum,AND,OR,NOTorMOD: see Choices without IF. - A number is digits with an optional point:
3,0.25. No1e-3. - A swizzle goes on a name, a word like
UV, or brackets, not straight on a function:c = OWNTEXTURE(UV)thenc.r. - A swizzle has one to four parts, all from
rgbaor all fromxyzw, and only the parts the thing has:UV.zdoesn’t exist. - You can’t change one part of a variable:
c.r = 1.0isn’t a line. Make a new color instead:c = RGBA(1.0, c.g, c.b, c.a). USElines come beforeWHEN DRAWING, and there’s oneWHEN 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
| What | How many |
|---|---|
Numbers in the settings, a USE NUMBER one and a USE COLOR four | 64 |
USE IMAGE pictures | 4 |
| Parts in a swizzle | 4 |
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,PXandOWNTEXTUREare 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
| Line | Means |
|---|---|
SHADER "name" … END SHADER | Defines 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 name | A picture setting, given by its name in shader_params, read with SAMPLE(name, uv). Up to 4. |
WHEN DRAWING … END WHEN | The code, run once for every pixel. |
name = expression | Makes or changes a variable. |
SET COLOR = expression | The pixel’s color: four parts, 0 to 1. The last one wins; with none, the pixel keeps IMAGE. |
' comment | To the end of the line. |
What a pixel knows
| Word | Parts | Is |
|---|---|---|
IMAGE | 4 | The color the pixel was going to be. |
UV | 2 | Where it is on what’s being drawn: (0, 0) top left, (1, 1) bottom right. On an animated sprite, on the frame. |
SCREEN | 2 | Where it is on the screen, in the game’s pixels, from (0, 0) at the game’s top left. |
SCREEN_SIZE | 2 | The game’s size, in its own pixels, as SCREEN set it. |
TIME | 1 | Seconds since the renderer started. |
TICK | 1 | Frames drawn since this shader went on this layer or sprite. |
Functions
| Function | Gives |
|---|---|
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
| On | Settings | Off | |
|---|---|---|---|
| A layer | SET LAYER n !! shader("name") | !! shader_params({...}), with shader or by itself | !! clear_shader |
| A sprite | SET SPRITE n !! shader("name") | !! shader_params({...}), with shader | !! clear_shader |
A shape, with a texture | SET POLYGON n !! shader("name") | !! shader_params({...}), with shader | !! clear_shader |
| A behavior | USE SHADER "name" | !! param("name") start(a) to(b) duration(ticks) [loop] | when the behavior ends |
| A transition | CLS [color] !! effect_shader("name") [speed(n)] | progress, target_r, target_g, target_b, target_a, filled in | when 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.