How to create a simple outline around a PNG sprite?

This feature was lately requested at github. There are many tutorials about this topic for Unity and also various plugins (like this one for example). So I thought it might be interesting to give it a try with three.js.

Instead of adding a new official example, it might be more flexible to experiment with the code at the forum first. The following fiddle demonstrates how to generate a thin inset outline for transparent PNG sprites:

Demo

The additional shader code is injected into SpriteMaterial via onBeforeCompile(). That means all other material features like fog or tonemapping should still work. It would be actually great to enhance the shader code so you can configure the width of the outline with an additional uniform. Besides, it would be also interesting to draw the outline effect around the actual colored texels (and thus not produce an inset outline). Right now, the new shader chunk is still quite simple since you can only change the outline color with a uniform. But it would be great if we (the community) could improve the code over time until we have something more fancy :blush:

8 Likes

Awesome! This seems like a great effect. It would be nice to use a sprite that had a crisper edge so it’s easier to to see the effect and, as you mentioned, a uniform for modifying the thickness of the outline. Is there a reason you added an inset outline instead of one outside the sprite?

1 Like

I’ve implemented the code according to the following two (Unity) resources.

Dev Diary - Research Report: 2D Sprite Outline Shaders
Writing Shaders In Unity - 2D Sprite Outline - Beginner Tutorial

Well, it was important to me to work with references^^. And since both explained the topic quite well, I decided to start with an inset outline. It seems drawing the outline around the sprite requires a different type of shader. I’ve not yet found a proper resource for that though. This is why I labeled the issue with help-wanted :innocent:.

1 Like

Great! I tried to make a few changes so that thickness, inset, and outset outlines are supported and I grabbed a different png off google with sharper edges:

image

https://jsfiddle.net/c78evfx4/5/

A couple other things I changed:

  • I added 4 more samples for the outline (8 now) because with the 4 at 90 degrees to each other you get artifacts as you increase the thickness. More samples decreases the visibility of those. This is maybe something that could be configured with material defines.
  • Because the offsets are calculated using the canvas resolution the thickness of them varies based on the aspect ration of the element. If the canvas is extra wide then the outline is thicker vertically then it is horizontally. Instead I’ve changed it to just use the y resolution to calculate the offsets, but it scales a bit oddly when resizing the window. Instead it might be best to pass in the texture resolution so it’s a consistent thickness for the sprite but I guess it depends on the effect you want.
1 Like

Cool! Well done! :+1:

Yes, I remember that one resource mentioned to use 8 instead of 4 samples for better quality. The few additional textures fetches should not have a noticeable performance impact in most cases.

BTW: Instead of computing the a variable for the inset outline like so a *= val;, I would use min to makes things more consistent: a = min(a, val);.

Yes, using the texture resolution is the better default. I’ve updated the fiddle so the respective uniform is updated with the dimensions of the texture in TextureLoader.onLoad(). Also refactored the uniform handling a bit.

Really nice topic. I wish I had some of this information long ago when I was starting out with shaders. I use essentially the same technique in my game for outline shader, but on meshes. You just first render its silhouette, and then it’s the same.


image

for sprites, i feel it would be better to bake the outline into the sprite, but that would require a lot of additional work and would mess with the sizes of the sprites too. It’s a trade-off.

I think you have to basically shrink the sprite by the outline width. And expand the drawn particle. Shrinking the sprite is simple enough. You need the have the resolution of the sprite inside the shader for that, so you can scale the UV and offset it.

Yes, that seems one way to do it. But the implemented approach by @gkjohnson seems to work quite well and does not need transformation steps. The fiddle in my last post supports both a normal outline and an inset effect.

It does, but without transforming the UV, you end up with clipping
image

1 Like

Yes this is a limitation of the approach. But if the sprite has enough transparent space, this error won’t occur.

1 Like

I agree fully. The sprites that I work with typically have aggressive crop though, so for the assets I’m used to working with - it’s a major point. Either way - very cool feature :slight_smile: