Crash BASIC Shader Guide

Crash BASIC Shader Guide

A beginner-friendly guide to creating custom visual effects using Crash BASIC's shader system.

What Are Shaders?

Shaders are small programs that run on your GPU (graphics card) and process every pixel of a sprite or layer. Think of them as Instagram filters for your game graphics - they can tint colors, create glows, add distortions, and much more.

Key concept: A shader runs once per pixel, and for each pixel, you decide what color it should be.

The Mental Model

Forget everything you know about procedural programming for a moment. Shaders work differently:

  1. No loops over pixels - The GPU runs your shader code simultaneously for every pixel (modern GPUs don't really do everything simultaneously, but for the right mental model, you can pretend that they do)
  2. No global state - Each pixel calculation is independent
  3. Think in coordinates - You get UV coordinates (0.0 to 1.0) telling you where this pixel is
  4. Think in colors - You get IMAGE (the original pixel color) and output SET COLOR
For each pixel on screen (handled by GPU, not you):
    UV = where this pixel is (0,0 = top-left, 1,1 = bottom-right)
    IMAGE = what color this pixel currently is
    ... your shader code runs here ...
    SET COLOR = the final color you want

Basic Shader Structure

SHADER "MyShader"
  ' Optional: declare parameters
  USE NUMBER brightness DEFAULT 1.0
  USE COLOR tint DEFAULT RGBA(1.0, 1.0, 1.0, 1.0)
  USE IMAGE noiseTexture

  WHEN DRAWING
    ' Your shader code here
    ' Must end with SET COLOR = ...
    SET COLOR = IMAGE
  END WHEN
END SHADER

Applying Shaders

' Apply to a sprite
SET SPRITE 1 SHADER "MyShader"
SET SPRITE 1 SHADER "MyShader", @brightness=0.5  ' with parameter

' Apply to an entire layer
SET LAYER 0 SHADER "MyShader"

' Remove shader
SET SPRITE 1 SHADER NONE

Built-in Variables

Variable Type Description
IMAGE vec4 (RGBA) The original pixel color at this position (0.0-1.0 range)
UV vec2 Texture coordinates. UV.x = 0.0 (left) to 1.0 (right), UV.y = 0.0 (top) to 1.0 (bottom)
TIME float Seconds elapsed since program started (for animations)
TICK float Frames since shader was applied (60fps, for frame-based animations)
SCREEN vec2 Fragment position in screen pixels (like gl_FragCoord). Useful for screen-space effects
SCREEN_SIZE vec2 Screen dimensions in pixels (width, height). Use SCREEN / SCREEN_SIZE for normalized screen coords

Understanding IMAGE

IMAGE contains the RGBA color of the current pixel:

  • IMAGE.r = red (0.0 to 1.0)
  • IMAGE.g = green (0.0 to 1.0)
  • IMAGE.b = blue (0.0 to 1.0)
  • IMAGE.a = alpha/transparency (0.0 = invisible, 1.0 = solid)

Important: Colors are 0.0-1.0, NOT 0-255!

Understanding UV

UV tells you where this pixel is located:

  • UV.x = 0.0 = left edge
  • UV.x = 1.0 = right edge
  • UV.y = 0.0 = top edge
  • UV.y = 1.0 = bottom edge
  • UV.x = 0.5, UV.y = 0.5 = center

Essential Functions

Color Creation

RGB(r, g, b)           ' Create color with alpha=1.0
RGBA(r, g, b, a)       ' Create color with custom alpha

Math Functions

SIN(x), COS(x)         ' Trigonometry (radians)
ABS(x)                 ' Absolute value
FLOOR(x)               ' Round down
FRACT(x)               ' Fractional part (x - floor(x))
POW(x, y)              ' x raised to power y
SQRT(x)                ' Square root
MIN(a, b), MAX(a, b)   ' Minimum/maximum
CLAMP(x, min, max)     ' Constrain x between min and max

Interpolation (Super Important!)

MIX(a, b, t)           ' Blend between a and b
                       ' t=0.0 returns a, t=1.0 returns b, t=0.5 returns halfway

STEP(edge, x)          ' Returns 0.0 if x < edge, else 1.0
SMOOTHSTEP(a, b, x)    ' Smooth transition from 0 to 1 as x goes from a to b

Pseudo-Random

RND(x, y)              ' Pseudo-random 0-1 from two seed values
                       ' Same inputs always produce same output (deterministic)
RND(seed)              ' Pseudo-random 0-1 from single seed value

' Examples:
RND(UV.x, UV.y)        ' Random per-pixel (static)
RND(cellX, cellY)      ' Random per-grid-cell (static)
RND(UV.x, UV.y + TIME) ' Random per-pixel (animated/flickering)

Vector Functions

VEC2(x, y)             ' Create 2D vector
VEC3(x, y, z)          ' Create 3D vector
VEC4(x, y, z, w)       ' Create 4D vector
LENGTH(v)              ' Vector magnitude
NORMALIZE(v)           ' Unit vector (length = 1)
DOT(a, b)              ' Dot product

Texture Sampling

SAMPLE(textureName, uv)  ' Sample a USE IMAGE texture at given UV
OWNTEXTURE(uv)           ' Sample the sprite's own texture at different UV
                         ' (useful for distortion effects)
PX(x, y)                 ' Convert pixel offset to UV offset

Swizzling (Rearranging Components)

color.r                ' Just the red component
color.rgb              ' RGB as vec3 (no alpha)
color.bgr              ' Blue, green, red (swapped)
uv.yx                  ' Swap x and y

Important limitation: Swizzles only work on variables and built-ins (IMAGE, UV, etc.), NOT directly on function results:

' CORRECT - store in variable first, then swizzle
n = SAMPLE(noise, UV)
value = n.r

' WRONG - will not parse!
' value = SAMPLE(noise, UV).r

' CORRECT - parenthesized expressions work
offset = (UV * 2.0).x

' WRONG - function calls don't
' offset = VEC2(1.0, 2.0).x

Example 1: Fade (Transparency Control)

The simplest shader - control how transparent something is.

SHADER "Fade"
  USE NUMBER alpha DEFAULT 1.0

  WHEN DRAWING
    c = IMAGE
    SET COLOR = RGBA(c.r, c.g, c.b, c.a * alpha)
  END WHEN
END SHADER

' Usage:
SET SPRITE 1 SHADER "Fade", @alpha=0.5   ' 50% transparent
SET SPRITE 1 SHADER "Fade", @alpha=0.0   ' Invisible
SET SPRITE 1 SHADER "Fade", @alpha=1.0   ' Fully visible

How it works:

  • IMAGE gives us the original pixel color
  • We multiply the alpha by our alpha parameter
  • Lower alpha = more transparent

Animated Fade (Pulsing)

SHADER "Pulse"
  USE NUMBER speed DEFAULT 2.0

  WHEN DRAWING
    c = IMAGE
    ' SIN oscillates between -1 and 1
    ' ABS makes it 0 to 1
    ' Result: smoothly pulses between invisible and visible
    alpha = ABS(SIN(TIME * speed))
    SET COLOR = RGBA(c.r, c.g, c.b, c.a * alpha)
  END WHEN
END SHADER

Example 2: Whiteout (Flash to White)

Makes a sprite flash white - perfect for damage effects.

SHADER "Whiteout"
  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

' Usage:
SET SPRITE 1 SHADER "Whiteout", @amount=0.0   ' Normal
SET SPRITE 1 SHADER "Whiteout", @amount=0.5   ' Half white
SET SPRITE 1 SHADER "Whiteout", @amount=1.0   ' Fully white

How it works:

  • MIX(a, b, t) blends between two colors
  • When amount=0, you see the original image
  • When amount=1, you see pure white
  • The alpha is preserved so transparent areas stay transparent

Auto-Flashing Whiteout

SHADER "DamageFlash"
  USE NUMBER speed DEFAULT 10.0

  WHEN DRAWING
    c = IMAGE
    white = RGBA(1.0, 1.0, 1.0, c.a)
    flash = ABS(SIN(TIME * speed))
    SET COLOR = MIX(c, white, flash)
  END WHEN
END SHADER

Colored Flash (Red for Damage)

SHADER "RedFlash"
  USE NUMBER amount DEFAULT 0.0

  WHEN DRAWING
    c = IMAGE
    red = RGBA(1.0, 0.0, 0.0, c.a)
    SET COLOR = MIX(c, red, amount)
  END WHEN
END SHADER

Example 3: Melt Effect (Doom-Style)

The melt effect shifts pixels downward at varying speeds, creating that classic Doom transition.

SHADER "Melt"
  USE NUMBER progress DEFAULT 0.0

  WHEN DRAWING
    ' Calculate how far this column should have "melted"
    ' Use UV.x to make each column melt at different rates
    columnOffset = SIN(UV.x * 20.0) * 0.3 + 0.7
    meltAmount = progress * columnOffset

    ' Sample from above the current position
    sampleY = UV.y - meltAmount

    ' If we're sampling from above the image, show black
    belowTop = STEP(0.0, sampleY)

    ' Sample the texture at the offset position
    c = OWNTEXTURE(VEC2(UV.x, sampleY))

    ' Black if we've "melted past" this point
    SET COLOR = RGBA(c.r * belowTop, c.g * belowTop, c.b * belowTop, c.a)
  END WHEN
END SHADER

' Animate with BSHADER in a behavior:
BEHAVIOR "meltAway"
  BSHADER "Melt" @progress=0.0 TO 1.5 DURATION 60
END BEHAVIOR

How it works:

  • OWNTEXTURE(uv) lets us sample the sprite at different coordinates
  • We offset the Y coordinate based on progress
  • Different columns melt at different speeds using SIN(UV.x * 20.0)
  • STEP(0.0, sampleY) creates a hard cutoff when we've scrolled past the top

Example 4: Random Purple Translucent Rectangles

This creates an overlay of random semi-transparent purple rectangles.

SHADER "PurpleRects"
  USE NUMBER rectSize DEFAULT 0.1
  USE NUMBER alpha DEFAULT 0.3

  WHEN DRAWING
    c = IMAGE

    ' Divide screen into grid cells
    cellX = FLOOR(UV.x / rectSize)
    cellY = FLOOR(UV.y / rectSize)

    ' Get pseudo-random value for this cell (0-1)
    random = RND(cellX, cellY)

    ' Only show rectangle if random > 0.5 (about 50% of cells)
    showRect = STEP(0.5, random)

    ' Purple color with variable alpha
    purple = RGBA(0.6, 0.0, 0.8, alpha * showRect)

    ' Blend purple over original image
    SET COLOR = MIX(c, purple, purple.a)
  END WHEN
END SHADER

How it works:

  • FLOOR(UV / rectSize) divides the screen into grid cells
  • RND(cellX, cellY) returns a pseudo-random value 0-1 for each cell
  • Each cell gets a consistent random value (same cell = same random every frame)
  • We use STEP(0.5, random) to show rectangles in ~50% of cells
  • MIX blends the purple overlay with the original image

Animated Random Rectangles

SHADER "AnimatedPurpleRects"
  USE NUMBER rectSize DEFAULT 0.1
  USE NUMBER alpha DEFAULT 0.3
  USE NUMBER speed DEFAULT 1.0

  WHEN DRAWING
    c = IMAGE

    cellX = FLOOR(UV.x / rectSize)
    cellY = FLOOR(UV.y / rectSize)
    seedOffset = FLOOR(TIME * speed)

    random = RND(cellX, cellY + seedOffset)
    showRect = STEP(0.5, random)

    purple = RGBA(0.6, 0.0, 0.8, alpha * showRect)
    SET COLOR = MIX(c, purple, purple.a)
  END WHEN
END SHADER

Example 5: Dissolve Effect

Pixels disappear based on a noise pattern - great for death animations.

SHADER "Dissolve"
  USE NUMBER progress DEFAULT 0.0
  USE IMAGE noise

  WHEN DRAWING
    c = IMAGE

    ' Sample noise texture to get random value for this pixel
    n = SAMPLE(noise, UV)

    ' If noise value is less than progress, make pixel transparent
    visible = STEP(progress, n.r)

    SET COLOR = RGBA(c.r, c.g, c.b, c.a * visible)
  END WHEN
END SHADER

' Usage requires a noise texture:
LOADIMG "noise.png", "noise"
SET SPRITE 1 SHADER "Dissolve", @progress=0.5

Dissolve Without External Texture

If you don't have a noise image, use RND to generate pseudo-random noise:

SHADER "DissolveSimple"
  USE NUMBER progress DEFAULT 0.0

  WHEN DRAWING
    c = IMAGE

    ' Generate noise from UV coordinates
    noise = RND(UV.x, UV.y)

    visible = STEP(progress, noise)
    SET COLOR = RGBA(c.r, c.g, c.b, c.a * visible)
  END WHEN
END SHADER

Example 6: CRT Monitor Effect

Scanlines and slight color separation for a retro look.

SHADER "CRT"
  USE NUMBER scanlineIntensity DEFAULT 0.3
  USE NUMBER rgbSeparation DEFAULT 0.003

  WHEN DRAWING
    ' Sample with slight RGB offset for chromatic aberration
    rSample = OWNTEXTURE(VEC2(UV.x + rgbSeparation, UV.y))
    gSample = OWNTEXTURE(UV)
    bSample = OWNTEXTURE(VEC2(UV.x - rgbSeparation, UV.y))
    r = rSample.r
    g = gSample.g
    b = bSample.b
    a = IMAGE.a

    ' Create scanlines (dark every other line)
    scanline = 1.0 - scanlineIntensity * STEP(0.5, FRACT(UV.y * 200.0))

    SET COLOR = RGBA(r * scanline, g * scanline, b * scanline, a)
  END WHEN
END SHADER

Using SCREEN Coordinates

SCREEN gives you the actual pixel position on screen (not within the sprite). This is useful for effects that should be consistent across multiple sprites or the whole screen.

Global Scanlines (Screen-Space)

Unlike the CRT example above which uses UV.y, this uses SCREEN.y so scanlines stay fixed on screen as sprites move:

SHADER "GlobalScanlines"
  USE NUMBER intensity DEFAULT 0.3

  WHEN DRAWING
    c = IMAGE
    ' Scanlines based on screen position, not texture position
    scanline = 1.0 - intensity * STEP(0.5, FRACT(SCREEN.y / 2.0))
    SET COLOR = RGBA(c.r * scanline, c.g * scanline, c.b * scanline, c.a)
  END WHEN
END SHADER

Screen-Space Dither Pattern

Creates a dithering pattern that's consistent across all sprites:

SHADER "Dither"
  USE NUMBER threshold DEFAULT 0.5

  WHEN DRAWING
    c = IMAGE
    ' 2x2 Bayer dither pattern based on screen position
    px = FLOOR(SCREEN.x)
    py = FLOOR(SCREEN.y)
    pattern = FRACT((px + py * 2.0) / 4.0)

    grey = (c.r + c.g + c.b) / 3.0
    visible = STEP(pattern, grey + threshold - 0.5)
    SET COLOR = RGBA(c.r * visible, c.g * visible, c.b * visible, c.a)
  END WHEN
END SHADER

Normalized Screen Position

To get 0-1 range for the whole screen (like UV but for screen space):

screenUV = SCREEN / SCREEN_SIZE  ' 0,0 = top-left, 1,1 = bottom-right

Common Patterns & Recipes

Distance from Center

center = VEC2(0.5, 0.5)
dist = LENGTH(UV - center)  ' 0 at center, ~0.707 at corners

Circular Gradient

dist = LENGTH(UV - VEC2(0.5, 0.5))
gradient = 1.0 - CLAMP(dist * 2.0, 0.0, 1.0)  ' 1 at center, 0 at edges

Vignette (Darken Edges)

dist = LENGTH(UV - VEC2(0.5, 0.5))
vignette = 1.0 - SMOOTHSTEP(0.3, 0.7, dist) * 0.5
SET COLOR = RGBA(c.r * vignette, c.g * vignette, c.b * vignette, c.a)

Greyscale

grey = (c.r + c.g + c.b) / 3.0
SET COLOR = RGBA(grey, grey, grey, c.a)

Invert Colors

SET COLOR = RGBA(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a)

Sepia Tone

grey = (c.r + c.g + c.b) / 3.0
r = MIN(grey * 1.2, 1.0)
g = grey
b = grey * 0.8
SET COLOR = RGBA(r, g, b, c.a)

Wave Distortion

offset = SIN(UV.y * 20.0 + TIME * 5.0) * 0.02
c = OWNTEXTURE(VEC2(UV.x + offset, UV.y))
SET COLOR = c

Pixelation

pixels = 32.0
pixelUV = FLOOR(UV * pixels) / pixels
SET COLOR = OWNTEXTURE(pixelUV)

Animating Shaders with BSHADER

Use BSHADER in behaviors to animate shader parameters:

SHADER "Dissolve"
  USE NUMBER progress DEFAULT 0.0
  WHEN DRAWING
    c = IMAGE
    noise = RND(UV.x, UV.y)
    visible = STEP(progress, noise)
    SET COLOR = RGBA(c.r, c.g, c.b, c.a * visible)
  END WHEN
END SHADER

BEHAVIOR "deathDissolve"
  BSHADER "Dissolve" @progress=0.0 TO 1.0 DURATION 60
END BEHAVIOR

' Apply to sprite when it dies:
SET SPRITE enemy BEHAVIOR "deathDissolve"

Debugging Tips

  1. Start simple - Get basic colors working before adding complexity
  2. Visualize UV - Use SET COLOR = RGBA(UV.x, UV.y, 0.0, 1.0) to see coordinates
  3. Check ranges - Remember colors are 0.0-1.0, not 0-255
  4. Test incrementally - Change one thing at a time
  5. Use CLAMP - Prevent colors going outside 0.0-1.0 range

Debug Shader: See UV Coordinates

SHADER "DebugUV"
  WHEN DRAWING
    SET COLOR = RGBA(UV.x, UV.y, 0.0, 1.0)
  END WHEN
END SHADER

Debug Shader: See Alpha Channel

SHADER "DebugAlpha"
  WHEN DRAWING
    a = IMAGE.a
    SET COLOR = RGBA(a, a, a, 1.0)
  END WHEN
END SHADER

Performance Notes

  • Shaders run on the GPU - they're very fast
  • Avoid extremely complex math if not needed
  • OWNTEXTURE is slightly more expensive than IMAGE
  • SAMPLE with external textures adds texture lookup cost
  • Simple shaders (tint, fade, flash) have negligible performance impact

Quick Reference

Want to... Use...
Make transparent Multiply c.a by a value 0.0-1.0
Flash to color MIX(c, targetColor, amount)
Animate over time Use TIME or TICK
Distort image OWNTEXTURE(modifiedUV)
Random per-pixel RND(UV.x, UV.y)
Grid pattern FLOOR(UV * gridSize)
Distance from point LENGTH(UV - point)
Smooth transition SMOOTHSTEP(start, end, value)
Hard cutoff STEP(threshold, value)
Oscillate SIN(TIME * speed)