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 y so your function matches the lessons.

Cheat sheet

IdeaWrite
Vectorsvec2 vec3 vec4
Declare a valuefloat d = length(p);
A functionfloat circle(vec2 p, float r) { ... }
Matricesmat2 mat3, multiply with M * v (this site writes v * M for lesson rotations)
Blendmix(a, b, t)
Fractional partfract(x)
Clamp to 0..1clamp(x, 0.0, 1.0)
Remaindermod(a, b) (always positive for positive b)
Choosec ? a : b
Angle of a vectoratan(y, x)
Time in secondstime
Pixel positionuv (screen_coords in effect() are pixels, top-left)
Width / heightaspect, or love_ScreenSize.x / love_ScreenSize.y
Entry pointvec4 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