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

Shaders and transitions

Everything so far has been about what I draw. This lesson is about how it looks once it’s drawn: a whole layer tinted for nighttime, a sprite that flashes white when it’s hit, a screen that melts away between one part of a game and the next.

A shader

A shader is a little program that runs once for every pixel of a layer or a sprite, on the graphics chip, and decides what color that pixel ends up. You write one in a SHADER block, and give it a name.

SCREEN 320, 240
SHADER "night"
    USE NUMBER dark DEFAULT 0.5
    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

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("night") shader_params({dark: 0.6})
PAUSE

Inside WHEN DRAWING, IMAGE is the pixel’s color as it was drawn, and whatever you SET COLOR to is what shows. Colors in here are RGBA with each part from 0 to 1, so c.r * 0.4 is red at forty percent. This one turns the red and the green down and leaves the blue, and the city goes blue and dark.

USE NUMBER is a setting the shader takes, with the value it has when nobody says. SET LAYER with !! shader(...) puts the shader on the layer, and shader_params gives it its settings by name.

Crash-tan’s Tip: A shader’s body is a language of its own, the one the graphics chip speaks, and I hand it over as you wrote it. It has MIX, SIN, CLAMP, STEP and friends, but none of your program’s variables: anything it needs from your game comes in through a USE setting.

Changing a setting as you go

The settings can change while the shader runs. Here the sun goes down and comes back up, by changing nothing but dark. The settings go with the shader they’re for, so every tick says both:

SCREEN 320, 240
SHADER "night"
    USE NUMBER dark DEFAULT 0.5
    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

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
hour = 0
DO
    IF COOLDOWN(1)
        hour = hour + 0.01
        SET LAYER 0 !! shader("night") shader_params({dark: 0.35 + sin(hour) * 0.35})
    END IF
LOOP

SET LAYER 0 !! clear_shader takes it off again.

A shader can also keep time by itself. TIME is how many seconds the game has been going, and OWNTEXTURE reads the pixel from somewhere else in the same picture. Read it from a little to the side, by a different amount on every row, and the city ripples like it’s under water:

SCREEN 320, 240
SHADER "water"
    USE NUMBER size DEFAULT 0.01
    WHEN DRAWING
        c = OWNTEXTURE(VEC2(UV.x + SIN(UV.y * 30.0 + TIME * 3.0) * size, UV.y))
        SET COLOR = c
    END WHEN
END SHADER

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

UV is where the pixel is, from (0, 0) at the top left to (1, 1) at the bottom right. Nothing in the program moves: the ripple is all the shader’s.

A shader on a sprite

A sprite takes a shader the same way, with SET SPRITE. Here are three heroes: one as it is, and two sharing a shader, at half strength and all the way.

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

LOADIMG "hero.png", "crashtan"
ENABLE SPRITE 0
SET SPRITE 0 !! image("crashtan") x(40) y(80) scale((2, 2))
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("crashtan") x(120) y(80) scale((2, 2))
    !! shader("flash") shader_params({amount: 0.5})
SHOW SPRITE 1
ENABLE SPRITE 2
SET SPRITE 2 !! image("crashtan") x(200) y(80) scale((2, 2))
    !! shader("flash") shader_params({amount: 1})
SHOW SPRITE 2
PAUSE

MIX(c, white, amount) is the way from one color to another: none of the way at 0, all of it at 1. Keeping c.a, the pixel’s alpha, keeps the see-through parts of the sprite see-through, so it’s the hero that turns white and not the whole square.

Flashing from a behavior

A shader can change over time on its own, too. USE SHADER, in a behavior, runs a shader while the behavior does, and takes one of its settings from one value to another over some ticks. With !! loop it goes back and forth for as long as the sprite has the behavior: a glow for a shield, or a power that’s about to run out.

SCREEN 320, 240
SHADER "flash"
    USE NUMBER amount DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        white = RGBA(1.0, 1.0, 1.0, c.a)
        SET COLOR = MIX(c, white, 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", "crashtan"
ENABLE SPRITE 0
SET SPRITE 0 !! image("crashtan") x(120) y(80) scale((2, 2))
    !! behavior("shielded")
SHOW SPRITE 0
PAUSE

The hero glows white and back, every half second, with the shield on. Without !! loop, it would go from start to to once, and stay there.

Clearing with style

CLS clears a layer at once. With !! effect, it takes its time about it: "fade", "melt", "block", "dissolve", "wipex" or "wipey". My renderer runs the effect by itself, and while it does, transition says which one it is. When it’s over, transition is "".

SCREEN 320, 240
LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
CLS 0 !! effect("melt")
PRINT "now running:", transition
now running: MELT

Here’s every one of them in turn. The city goes up, stays for a second, and goes away with the next effect:

SCREEN 320, 240
LOADIMG "city.png", "city"
effects = ["fade", "melt", "block", "dissolve", "wipex", "wipey"]
n = 0
showing = FALSE
DO
    IF COOLDOWN(60) AND transition = ""
        IF showing
            CLS 0 !! effect(effects(n))
            n = (n + 1) MOD LEN(effects)
        ELSE
            FRAME
                CLS
                PUTIMAGE (32, 0), "city"
                LOCATE 1, 1
                PRINT "next:", effects(n)
            END FRAME
        END IF
        showing = NOT showing
    END IF
LOOP

!! speed says how fast an effect goes, and !! size how big the blocks are for "block". Each layer runs a transition of its own, and transition(1) asks about layer 1, whichever layer you’re drawing on.

Crash-tan’s Tip: A transition doesn’t stop your program: it runs on my renderer while your code goes on. When the next thing should wait for it, wait in your loop until transition = "", the way this one does before it draws the city again.

Clearing to a picture

!! to_image clears a layer to a picture instead of a color. With an effect, that’s a transition from what’s there into the picture: a dissolve from black into the title screen, say.

SCREEN 320, 240
LOADIMG "city.png", "city"
CLS 0 !! effect("dissolve") to_image("city")
PAUSE

Shaders and transitions are how a game looks between its parts. The parts themselves, the title screen, the level, the game over, are scenes, and they’re next.