Fragment shaders
A fragment shader is a function that answers one question: what colour is the pixel at x, y? You write the function; gfx.fill runs it for every pixel on the panel.
function fragment(x, y, u) return x / 63, y / 63, 0.25end
function _draw() gfx.fill(shader or fragment)endThat draws a gradient: more red to the right, more green towards the bottom.
xandyare the pixel’s coordinates, as numbers starting at 0.uis theuniformstable.- The function returns red, green and blue as numbers from 0 to 1, not 0 to 255. Values outside that range are clamped.
gfx.fill(shader or fragment)uses a compiled version of the function when there is one and the function itself otherwise. The next page explains the difference. Until then, read it asgfx.fill(fragment).
Why write it this way
Section titled “Why write it this way”A shader has no loops over pixels and no state. Each pixel is computed from its position alone, which is what lets the panel run the same function for many pixels at once. It is also a compact way to describe a picture: the plasma further down is four lines.
Animating
Section titled “Animating”A shader cannot remember anything between frames, so time comes in as a uniform. Declare it in a comment, advance it in _update, read it in the shader:
-- @uniform time float
function _update(dt) uniforms.time = (uniforms.time or 0) + dtend
function fragment(x, y, u) local wave = 0.5 + 0.5 * sin(x * 0.2 + u.time * 3) return wave, 0.2, 1 - waveend
function _draw() gfx.fill(shader or fragment)endThe -- @uniform name type line tells the shader compiler what u.time is. The type is float, vec2, vec3 or vec4. It is only a comment to Lua, but get in the habit of writing one for every uniform the shader reads.
Vectors
Section titled “Vectors”Positions and colours come in groups, so shaders have vector types: vec2, vec3 and vec4.
-- @uniform resolution vec2
function _init() uniforms.resolution = vec2(gfx.width(), gfx.height())end
function fragment(x, y, u) local p = vec2(x, y) / u.resolution -- 0..1 across the panel local centre = p - 0.5 -- -0.5..0.5, zero in the middle local d = length(centre) -- distance from the middle return vec3(1 - d * 2, p.y, 0.3) -- one vec3 instead of three numbersend
function _draw() gfx.fill(shader or fragment)endWhat you can do with them:
- Build one from numbers or smaller vectors:
vec3(1, 0.5, 0),vec3(p, 1), orvec3(0.5)for three copies of one number. - Do arithmetic component by component:
a + b,a * b,a / b. A number combines with a vector of any size:p * 2,p - 0.5. - Pick components by name:
p.x,p.y, or several at once:p.xy,p.yx,c.rgb. The namesx y z wandr g b aare interchangeable. - Pass them to the math functions, which then work on each component:
sin(p),abs(p),mix(a, b, 0.5).
The built-in functions
Section titled “The built-in functions”These are available as plain names inside and outside a shader. The full list with descriptions is on the Shader built-ins page.
| Group | Functions |
|---|---|
| Trigonometry | sin cos tan asin acos atan |
| Powers | sqrt pow exp log |
| Rounding | floor ceil fract abs sign mod |
| Ranges | min max clamp step smoothstep mix |
| Vectors | dot length distance normalize cross |
| Noise | hash noise fbm |
Three that do a lot of work in shaders:
mix(a, b, t)blends fromatobastgoes from 0 to 1.smoothstep(lo, hi, v)is 0 belowlo, 1 abovehi, and a smooth ramp in between. It turns a distance into a soft edge.fbm(x, y, octaves)is layered noise: clouds, smoke, terrain.
Three shaders
Section titled “Three shaders”A plasma, the classic:
-- @uniform time floatlocal PI = 3.14159265
function _update(dt) uniforms.time = (uniforms.time or 0) + dtend
function fragment(x, y, u) local t = u.time local v = sin(x * 0.1 + t) + sin((y * 0.1 + t) * 0.7) + sin((x + y) * 0.05 + t) return 0.5 + 0.5 * sin(v * PI), 0.5 + 0.5 * cos(v * PI), 0.5 + 0.5 * sin(v * PI + 2.0)end
function _draw() gfx.fill(shader or fragment)endA soft ring, using a helper function and smoothstep:
-- @uniform time float-- @uniform resolution vec2
local function ring(p, radius) return smoothstep(0.06, 0.0, abs(length(p) - radius))end
function _init() uniforms.time = 0 uniforms.resolution = vec2(gfx.width(), gfx.height())end
function _update(dt) uniforms.time = uniforms.time + dtend
function fragment(x, y, u) local p = vec2(x, y) / u.resolution - 0.5 local glow = ring(p, 0.25 + 0.1 * sin(u.time)) return mix(vec3(0.05, 0.0, 0.2), vec3(1.0, 0.8, 0.4), glow)end
function _draw() gfx.fill(shader or fragment)endDrifting clouds from noise:
-- @uniform time float
function _update(dt) uniforms.time = (uniforms.time or 0) + dt * 0.3end
function fragment(x, y, u) local v = fbm(x * 0.05, y * 0.05 + u.time, 4) return v, v * 0.8, v * 0.5end
function _draw() gfx.fill(shader or fragment)endMixing a shader with drawing
Section titled “Mixing a shader with drawing”gfx.fill is just another drawing call. Fill the background with a shader, then draw on top:
-- @uniform time float
function _init() uniforms.time = 0 ball = { x = 5, y = 5, vx = 20, vy = 13 }end
function _update(dt) uniforms.time = uniforms.time + dt ball.x = ball.x + ball.vx * dt ball.y = ball.y + ball.vy * dt if ball.x < 0 or ball.x > gfx.width() - 3 then ball.vx = -ball.vx end if ball.y < 0 or ball.y > gfx.height() - 3 then ball.vy = -ball.vy endend
function fragment(x, y, u) local n = fbm(x * 0.05, y * 0.05 + u.time * 0.2, 3) return n * 0.2, n * 0.1, n * 0.5end
function _draw() gfx.fill(shader or fragment) gfx.rectfill(math.floor(ball.x), math.floor(ball.y), 3, 3, 255, 220, 60)endHow fast is it
Section titled “How fast is it”Run as written, a shader is slow: the panel makes a Lua call for each of the 4,096 pixels. The plasma above, which uses only plain numbers, manages about 8 frames per second that way, and the ring and the vector example go over the per-frame work limit and stop. Compiled, all of them run, the plasma at about 30 frames per second. That is the subject of the next page.
Next: Interpreted and compiled.