Page 1 of 2

Anyone have a shader to put an outline around an image?

Posted: Mon May 28, 2018 11:23 pm
by Jasoco
I’m looking for a shaded that can put an outline around the pixels in an image preferably with options to set the color and thickness if possible. Has anyone made one? I know they can be useful.

Re: Anyone have a shader to put an outline around an image?

Posted: Tue May 29, 2018 12:41 am
by pgimeno

Re: Anyone have a shader to put an outline around an image?

Posted: Tue May 29, 2018 3:56 pm
by var77

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 1:48 am
by Jasoco
Neither of those are working right at all.

The qwooke one is completely broken. Or I'm not understanding how to send the information to it.

And the blog one doesn't draw the way it should. It seems to draw the outline inside the image edge instead of around the outside of the image edge which is what I need.

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 6:23 am
by grump
The best way to do high-quality outlines is by applying a distance transform to the image, then use the resulting signed distance field to render the outline. It's an expensive operation that is not easy to do on a GPU. It's not suitable for realtime generated images and takes a lot of memory, because you need to precalculate the distance fields for each image.

Here's another less expensive technique I used before to put outlines around fonts. It samples the image multiple times and uses two passes to create an outlined image; one pass to render the outline, the second pass renders the image on top of it. It's still expensive and I haven't used this for realtime stuff, only to pre-bake outlines into textures. It may or may not be sufficiently fast for your application.

You can set varying thickness (size), color and smoothness of the outline. Lower smoothness gives crisp outlines, higher smoothness creates a glow-like effect.

Code: Select all

local gfx = love.graphics

local shader = gfx.newShader([[
	extern vec2 pixelsize;
	extern float size = 1;
	extern float smoothness = 1;

	vec4 effect(vec4 color, Image texture, vec2 uv, vec2 fc) {
		float a = 0;
		for(float y = -size; y <= size; ++y) {
			for(float x = -size; x <= size; ++x) {
				a += Texel(texture, uv + vec2(x * pixelsize.x, y * pixelsize.y)).a;
			}
		}
		a = color.a * min(1, a / (2 * size * smoothness + 1));

		return vec4(color.rgb, a);
	}
]])

-- the image
local canvas = gfx.newCanvas(128, 128)
canvas:renderTo(function()
	gfx.setColor(1, 0, 0)
	gfx.setLineWidth(20)
	gfx.circle('line', 64, 64, 32)
end)

function love.draw()
	gfx.setBlendMode('alpha')

	-- pass 1: render outline
	gfx.setColor(1, 1, 1) -- outline color
	shader:send('pixelsize', { 1 / canvas:getWidth(), 1 / canvas:getHeight() })
	shader:send('size', 4)
	gfx.setShader(shader)
	gfx.draw(canvas)

	-- pass 2: render image
	gfx.setBlendMode('alpha', 'premultiplied')
	gfx.setShader()
	gfx.setColor(1, 1, 1)
	gfx.draw(canvas)
end
Image

Edit: formatting
Edit2: there was a bug in the alpha calculation

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 6:40 am
by KayleMaster
A very dirty trick is to draw two copies of the sprite behind it, one smaller for the inner glow and one larger for the outer glow.
Just do setColor to whatever you want before hand.The thickness is basically the scale.
I don't know if this will work in all scenarios tho.

Either way any of the other solutions posted here will be much better. (like grump's)

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 7:47 am
by pgimeno
Jasoco wrote: Wed May 30, 2018 1:48 am And the blog one doesn't draw the way it should. It seems to draw the outline inside the image edge instead of around the outside of the image edge which is what I need.
Simply inverting the sign works for me:

outline.glsl

Code: Select all

vec4 resultCol;
extern vec2 stepSize;

vec4 effect( vec4 col, Image texture, vec2 texturePos, vec2 screenPos )
{
	// get color of pixels:
	number alpha = -4.0*texture2D( texture, texturePos ).a;
	alpha += texture2D( texture, texturePos + vec2( stepSize.x, 0.0f ) ).a;
	alpha += texture2D( texture, texturePos + vec2( -stepSize.x, 0.0f ) ).a;
	alpha += texture2D( texture, texturePos + vec2( 0.0f, stepSize.y ) ).a;
	alpha += texture2D( texture, texturePos + vec2( 0.0f, -stepSize.y ) ).a;

	// calculate resulting color
	resultCol = vec4( 0.4f, 1.0f, 0.1f, alpha );
	// return color for current pixel
	return resultCol;
}
You can see the outline of the next quad, though, so that's something to watch out for.

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 7:51 am
by bobbyjones
grump wrote: Wed May 30, 2018 6:23 am The best way to do high-quality outlines is by applying a distance transform to the image, then use the resulting signed distance field to render the outline. It's an expensive operation that is not easy to do on a GPU. It's not suitable for realtime generated images and takes a lot of memory, because you need to precalculate the distance fields for each image.

Here's another less expensive technique I used before to put outlines around fonts. It samples the image multiple times and uses two passes to create an outlined image; one pass to render the outline, the second pass renders the image on top of it. It's still expensive and I haven't used this for realtime stuff, only to pre-bake outlines into textures. It may or may not be sufficiently fast for your application.

You can set varying thickness (size), color and smoothness of the outline. Lower smoothness gives crisp outlines, higher smoothness creates a glow-like effect.

Code: Select all

local gfx = love.graphics

local shader = gfx.newShader([[
	extern vec2 pixelsize;
	extern float size = 1;
	extern float smoothness = 1;

	vec4 effect(vec4 color, Image texture, vec2 uv, vec2 fc) {
		float a = 0;
		for(float y = -size; y <= size; ++y) {
			for(float x = -size; x <= size; ++x) {
				a += Texel(texture, uv + vec2(x * pixelsize.x, y * pixelsize.y)).a;
			}
		}
		a = color.a * min(1, a / (2 * size * smoothness + 1));

		return vec4(color.rgb, a);
	}
]])

-- the image
local canvas = gfx.newCanvas(128, 128)
canvas:renderTo(function()
	gfx.setColor(1, 0, 0)
	gfx.setLineWidth(20)
	gfx.circle('line', 64, 64, 32)
end)

function love.draw()
	gfx.setBlendMode('alpha')

	-- pass 1: render outline
	gfx.setColor(1, 1, 1) -- outline color
	shader:send('pixelsize', { 1 / canvas:getWidth(), 1 / canvas:getHeight() })
	shader:send('size', 4)
	gfx.setShader(shader)
	gfx.draw(canvas)

	-- pass 2: render image
	gfx.setBlendMode('alpha', 'premultiplied')
	gfx.setShader()
	gfx.setColor(1, 1, 1)
	gfx.draw(canvas)
end
Image

Edit: formatting
Edit2: there was a bug in the alpha calculation
When I saw this post the first thing I did was ask the people on IRC if SDF will be faster than the way already described here lol. The only reason I did not recommend it is because no one has created a way to generate a SDF in love yet. So I was going to write one first before submitting my answer.

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 8:16 am
by grump
bobbyjones wrote: Wed May 30, 2018 7:51 amno one has created a way to generate a SDF in love yet
I implemented this for FÖNTGen, but I it doesn't work with LÖVE 11 yet. It's a pure Lua implementation that runs on a single core and while it's fast enough for the things I use it for, it's too slow to be used in realtime on a large scale.

Re: Anyone have a shader to put an outline around an image?

Posted: Wed May 30, 2018 7:00 pm
by Jasoco
pgimeno wrote: Wed May 30, 2018 7:47 am
Jasoco wrote: Wed May 30, 2018 1:48 am And the blog one doesn't draw the way it should. It seems to draw the outline inside the image edge instead of around the outside of the image edge which is what I need.
Simply inverting the sign works for me:

outline.glsl

Code: Select all

vec4 resultCol;
extern vec2 stepSize;

vec4 effect( vec4 col, Image texture, vec2 texturePos, vec2 screenPos )
{
	// get color of pixels:
	number alpha = -4.0*texture2D( texture, texturePos ).a;
	alpha += texture2D( texture, texturePos + vec2( stepSize.x, 0.0f ) ).a;
	alpha += texture2D( texture, texturePos + vec2( -stepSize.x, 0.0f ) ).a;
	alpha += texture2D( texture, texturePos + vec2( 0.0f, stepSize.y ) ).a;
	alpha += texture2D( texture, texturePos + vec2( 0.0f, -stepSize.y ) ).a;

	// calculate resulting color
	resultCol = vec4( 0.4f, 1.0f, 0.1f, alpha );
	// return color for current pixel
	return resultCol;
}
You can see the outline of the next quad, though, so that's something to watch out for.
See that's a problem I'd not want to deal with. Also using this shader doesn't work right either. I'm sending a table with two values to it and no matter what I send it doesn't render right.

bobbyjones wrote: Wed May 30, 2018 7:51 am
grump wrote: Wed May 30, 2018 6:23 am The best way to do high-quality outlines is by applying a distance transform to the image, then use the resulting signed distance field to render the outline. It's an expensive operation that is not easy to do on a GPU. It's not suitable for realtime generated images and takes a lot of memory, because you need to precalculate the distance fields for each image.

Here's another less expensive technique I used before to put outlines around fonts. It samples the image multiple times and uses two passes to create an outlined image; one pass to render the outline, the second pass renders the image on top of it. It's still expensive and I haven't used this for realtime stuff, only to pre-bake outlines into textures. It may or may not be sufficiently fast for your application.

You can set varying thickness (size), color and smoothness of the outline. Lower smoothness gives crisp outlines, higher smoothness creates a glow-like effect.

Code: Select all

local gfx = love.graphics

local shader = gfx.newShader([[
	extern vec2 pixelsize;
	extern float size = 1;
	extern float smoothness = 1;

	vec4 effect(vec4 color, Image texture, vec2 uv, vec2 fc) {
		float a = 0;
		for(float y = -size; y <= size; ++y) {
			for(float x = -size; x <= size; ++x) {
				a += Texel(texture, uv + vec2(x * pixelsize.x, y * pixelsize.y)).a;
			}
		}
		a = color.a * min(1, a / (2 * size * smoothness + 1));

		return vec4(color.rgb, a);
	}
]])

-- the image
local canvas = gfx.newCanvas(128, 128)
canvas:renderTo(function()
	gfx.setColor(1, 0, 0)
	gfx.setLineWidth(20)
	gfx.circle('line', 64, 64, 32)
end)

function love.draw()
	gfx.setBlendMode('alpha')

	-- pass 1: render outline
	gfx.setColor(1, 1, 1) -- outline color
	shader:send('pixelsize', { 1 / canvas:getWidth(), 1 / canvas:getHeight() })
	shader:send('size', 4)
	gfx.setShader(shader)
	gfx.draw(canvas)

	-- pass 2: render image
	gfx.setBlendMode('alpha', 'premultiplied')
	gfx.setShader()
	gfx.setColor(1, 1, 1)
	gfx.draw(canvas)
end
Image

Edit: formatting
Edit2: there was a bug in the alpha calculation
When I saw this post the first thing I did was ask the people on IRC if SDF will be faster than the way already described here lol. The only reason I did not recommend it is because no one has created a way to generate a SDF in love yet. So I was going to write one first before submitting my answer.
This one works perfectly though, but it also seems to require padding on the image to work right which would require a lot more effort on my part to redo all my image files. I hadn't planned on doing that. But it's a start. Even if I have to re-render all my images onto canvases with the padding automatically.