LÖVE (Love2D) shaders
LÖVE games are written in Lua, but shaders are GLSL with a few LÖVE conveniences, such as the effect entry point and the number type. The export wraps your function in effect() and tells you how to send time from Lua.
Things to know about GLSL
- The entry point is
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords). - Send values from Lua with
shader:send("time", love.timer.getTime()). - Screen coordinates start at the top-left; the wrapper flips
yso your function matches the lessons.
Cheat sheet
| Idea | Write |
|---|---|
| Vectors | vec2 vec3 vec4 |
| Declare a value | float d = length(p); |
| A function | float circle(vec2 p, float r) { ... } |
| Matrices | mat2 mat3, multiply with M * v (this site writes v * M for lesson rotations) |
| Blend | mix(a, b, t) |
| Fractional part | fract(x) |
| Clamp to 0..1 | clamp(x, 0.0, 1.0) |
| Remainder | mod(a, b) (always positive for positive b) |
| Choose | c ? a : b |
| Angle of a vector | atan(y, x) |
| Time in seconds | time |
| Pixel position | uv (screen_coords in effect() are pixels, top-left) |
| Width / height | aspect, or love_ScreenSize.x / love_ScreenSize.y |
| Entry point | vec4 frag(vec2 uv), called from effect() |
Handy built-ins: sin cos abs sign floor ceil fract step smoothstep clamp min max pow exp sqrt length distance dot cross normalize reflect
Gotchas
- Every pixel runs on its own, with no memory of other pixels.
- Declare a function above the place you use it.
- Prefer 2.0 over 2 in maths. This site fixes most cases for you; real engines may not.
- Loops need a small fixed count: for (int i = 0; i < 5; i++).
- LÖVE calls effect() for every pixel. The starter turns screen_coords into a bottom-left uv and calls your frag(). The wrapper doesn't count towards your score.
Example: a glowing ring
The same shader as it is written for LÖVE (Lua):
extern number time;
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
float d = length(p);
float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
vec3 bg = vec3(0.08, 0.08, 0.12);
vec3 fg = vec3(0.4, 0.9, 1.0);
return vec4(mix(bg, fg, m), 1.0);
}
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
// LÖVE's screen_coords start at the top-left, the lessons use bottom-left
vec2 size = love_ScreenSize.xy;
return frag(vec2(screen_coords.x, size.y - screen_coords.y) / size);
} Exporting to LÖVE
Save as shader.glsl. In main.lua: shader = love.graphics.newShader("shader.glsl"); in love.draw(): if shader:hasUniform("time") then shader:send("time", love.timer.getTime()) end (LÖVE refuses to send a value the shader does not use); love.graphics.setShader(shader); love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions()); love.graphics.setShader().
// ShaderBattle: Glowing ring
#define aspect (love_ScreenSize.x / love_ScreenSize.y)
extern number time;
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
float d = length(p);
float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
vec3 bg = vec3(0.08, 0.08, 0.12);
vec3 fg = vec3(0.4, 0.9, 1.0);
return vec4(mix(bg, fg, m), 1.0);
}
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
// LÖVE's screen_coords start at the top-left, the lessons use bottom-left
vec2 size = love_ScreenSize.xy;
return frag(vec2(screen_coords.x, size.y - screen_coords.y) / size);
} Questions
Does the wrapper count towards the code length score?
No. The engine boilerplate is free, so you are scored on the same shader code in every language.
Other languages
Unity (Built-in) · Unity (URP) · Unreal · Shadertoy / WebGL · OpenGL (LWJGL, raylib) · three.js · GameMaker · Godot · Apple (iOS, macOS) · WebGPU / Bevy