Outlines
Caution
This guide is made for LÖVE 12.0!
For an introduction to shaders, see: shaders
Outlines can be used to make objects stand out from the background, or to create a more cartoony look.
There are almost an infinite amount of ways to create an outline shader but we'll cover two simple ones here.
The first implementation requires that objects with an outline are drawn to a black canvas, which a shader then calculates the outlines for and overlays over the game.
outlineShader.fs
// radius in pixels of the outline
uniform float radius;
uniform vec4 OutlineColor;
// Increasing the bias will change how dark a pixel needs to be to be considered outside
// 0.0 is the darkest possible color, 1.0 is the lightest possible color
const float bias = 0.01;
bool outside(Image tex, vec2 texture_coords)
{
vec3 color = Texel(tex, texture_coords).rgb;
return color.r <= bias && color.g <= bias && color.b <= bias;
}
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) {
// Make sure we only color outside of the shape
if (!outside(tex, texture_coords))
return vec4(0.0, 0.0, 0.0, 0.0);
// Size of the outline in pixels
int size = int(radius);
vec2 texelSize = 1.0 / vec2(love_ScreenSize.xy);
// Loop over all pixels within the radius of the outline
// If any are inside the shape, color the pixel
for (int x = -size; x <= size; x++)
{
for (int y = -size; y <= size; y++)
{
if (x * x + y * y <= size * size && !(x == 0 && y == 0))
{
vec2 offset = vec2(x, y) * texelSize;
if (!outside(tex, texture_coords + offset))
return OutlineColor;
}
}
}
return vec4(0.0, 0.0, 0.0, 0.0);
}
main.lua
local outlineShader = love.graphics.newShader("outlineShader.fs")
function outline(gameCanvas)
love.graphics.setShader(outlineShader)
local mode, alphaMode = love.graphics.getBlendMode()
love.graphics.setBlendMode("alpha", "premultiplied")
-- Set the shader's uniform variables
outlineShader:send("radius", 2)
outlineShader:send("OutlineColor", { 1, 0, 0, 1 })
-- Draw the game canvas
love.graphics.draw(gameCanvas)
-- Clean up
love.graphics.setShader()
love.graphics.setBlendMode(mode, alphaMode)
end
The next method does not need a contrast between our image and pure black to work. This one works by calculating the average of the difference in color around the center pixel and applying the outline color if it crosses a certain threshold.
outlineShader.fs
// radius in pixels of the outline
uniform float radius;
uniform vec4 OutlineColor;
uniform float Threshold;
float difference(Image tex, vec2 texture_coords, vec4 color)
{
vec3 sampleColor = Texel(tex, texture_coords).rgb;
return distance(sampleColor, color.rgb);
}
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) {
// Size of the outline in pixels
int size = int(radius);
vec2 texelSize = 1.0 / vec2(love_ScreenSize.xy);
vec4 texColor = Texel(tex, texture_coords);
float diffSum = 0.0;
float count = 0.0;
// Loop over all pixels within the radius of the outline
// If any are inside the shape, color the pixel
for (int x = -size; x <= size; x++)
{
for (int y = -size; y <= size; y++)
{
if (x * x + y * y <= size * size && !(x == 0 && y == 0))
{
vec2 offset = vec2(x, y) * texelSize;
float diff = difference(tex, texture_coords + offset, texColor);
diffSum += diff;
count++;
}
}
}
if (diffSum / count >= Threshold)
{
return OutlineColor;
}
return vec4(0.0, 0.0, 0.0, 0.0);
}
local outlineShader = love.graphics.newShader("outlineShader.fs")
local outlineColor = { 1, 0, 0, 0.7 }
function outline(gameCanvas)
love.graphics.setShader(outlineShader)
local mode, alphaMode = love.graphics.getBlendMode()
love.graphics.setBlendMode("alpha", "alphamultiply")
-- Set the shader's uniform variables
outlineShader:send("radius", 2.5)
outlineShader:send("OutlineColor", outlineColor)
outlineShader:send("Threshold", 0.35)
-- Draw the game canvas
love.graphics.draw(gameCanvas)
-- Clean up
love.graphics.setShader()
love.graphics.setBlendMode(mode, alphaMode)
end