2D outline pixel shader on a pixel font

Hello

I’m really new to hlsl and I’ve been experimenting with it lately. I use a custom pixel font in my projects with multiple outlines, as seen below. I’m curious if it’s possible to recreate these examples using a hlsl pixel shader on a single spritebatch.drawstring result. The outlines are now drawn using many spritebatch.drawstring calls which is not so optimal. A shader would probably be faster, especially in the second example.

Some problems:

  • I saw that each character is drawn as its own texture. I would have to draw pixels outside the texture box of characters. I don’t know if this is possible.
  • The texture size of each string/character would be needed to check the color of neighbouring pixels. (to create the outline(s), as far as I know)
  • I need to know how many pixels actually represent one “font pixel”.
  • Some outlines need to be layered on top of others (see example 2).

Is a shader like this possible?
Thanks in advance.


1 Like

In my opinion it is better to write own font preprocessing or find font with outline than doing this realtime. However shader is definitely possible, basically what you would be doing is convolution (same approach that is used for instance to do blur effect).

Edit: Quick search shows that there were few sprite font preprocessors for XNA that added outline to the font, however I didn’t found any that I would think would still work or was downloadable from reliable source.

Yeah. I’ve been looking for some examples but haven’t found any. I can’t find much information about this.

Use signed distance field fonts.

You can do a hell of a lot with them.

Normally you would just test the alpha value and if it greater than a chosen value, set the pixel.

However you can do ranges.

if (alpha > 0.7f)  vResult = outline colour
if (alphs > 0.8f) vRexult = font colour

Would produce outlined chars

Then you can get creative …

float test1 = 0.1f * sin(time);
if (alpha > 0.6f + test1) vResult = outline colour

Would produce an animated outline.

You are really only limited by your imagination.

1 Like

Thank you!

I’ll try this asap :slight_smile:
Edit: Is there any information source for signed distance field fonts? I’m not familiar with it.

http://southseagamesgurus.co.uk/downloads.html

Font rendering
A little demo of how to do signed distance field fonts in Monogame

1 Like

Thank you for the demo!

@StainlessTobii

I’ve looked at the demo. Thanks for sharing, I learned something new today. :stuck_out_tongue:

Are SDF fonts not overkill for my effect?
I would have to create specific .png images and write my own .txt files for the font because of the double outlines/shadows. So I can’t just generate it with a tool, or is there another method/way?

imho SDF fonts are not only overkill, but you won’t get the effect you’re trying to get.

SDF looks absolutely stunning but you have no control over how the shadow will look other than a float value used to decide the width of the border (but not the shape, in example you could get the border of the letter R ok, but get the border of the letter O slightly rounded)

While this is usually desirable for fonts, you’re probably going for a ‘pixel look’ font, and SDF borders may not work for that.

Thanks for the reply @KakCAT.
That’s what I was thinking. I hope there’s another way to achieve this effect.
And yes, I’m going for a pixel look font.

While I love distance field fonts, even their best variant still have issues when rendering at very small sizes. It is great for use in world space text like nameplates and dynamic assets, but for GUI, especially gui with small/fine fonts it is still quite problematic.

Btw that’s case even for superior multi channel signed distance field fonts.

Also sorry, but that example actually starts to fall apart at large sizes as well.

There is example with mutli channel signed fonts: https://github.com/roy-t/MSDF however, while providing better quality, it creates interesting “font background” artifact at lower sizes. So I agree with you, I think it is overkill for your case and if you really want to do it realtime and you are not afraid about performance (honestly unless rest of the game is performance hungry or you draw ridiculous amount of text with overdraw) then use normal sprite fonts and drag it through one pass 3x3 kernel, provided you need just single pixel outline (which can be ofc upscaled up for pixel art look).

I’m just about to head to bed but I figured I’d post this and let you play with it. I’ve never tried this with text before… lol. Anyway, I created this shader for putting an outline around sprites. In theory, it should work just fine on text. If you want to add shadow, you’d have to adapt it.

Pass in as parameters the colour of the outline (xOutlineColour) and the size of the texture it will process (xTextureSize). I’m actually not sure what this will be for text rendering. You might actually have to render all your text to a RenderTarget layer and then run this over it? Is this the problem you were running into in your original post?

#if OPENGL
	#define SV_POSITION POSITION
	#define VS_SHADERMODEL vs_3_0
	#define PS_SHADERMODEL ps_3_0
#else
	#define VS_SHADERMODEL vs_4_0_level_9_1
	#define PS_SHADERMODEL ps_4_0_level_9_1
#endif

Texture2D SpriteTexture;
float2 xTextureSize : VPOS;
float4 xOutlineColour = float4(128, 0, 0, 255);

sampler2D InputSampler = sampler_state
{
	Texture = <SpriteTexture>;
};

struct VertexShaderOutput
{
	float4 Position : SV_POSITION;
	float4 Color : COLOR0;
	float2 UV : TEXCOORD0;
};

float4 MainPS(VertexShaderOutput input) : COLOR
{
	float4 currentPixel = tex2D(InputSampler, input.UV) * input.Color;
	float4 output = currentPixel;

	if (currentPixel.a == 0.0f)
	{
		float2 uvPix = float2(1 / xTextureSize.x, 1 / xTextureSize.y);

		if (false
			|| tex2D(InputSampler, float2((1 * uvPix.x) + input.UV.x, (0 * uvPix.y) + input.UV.y)).a > 0
			|| tex2D(InputSampler, float2((0 * uvPix.x) + input.UV.x, (1 * uvPix.y) + input.UV.y)).a > 0
			|| tex2D(InputSampler, float2((-1 * uvPix.x) + input.UV.x, (0 * uvPix.y) + input.UV.y)).a > 0
			|| tex2D(InputSampler, float2((0 * uvPix.x) + input.UV.x, (-1 * uvPix.y) + input.UV.y)).a > 0
		)
		{
			output = xOutlineColour;
		}
	}

	return output;
}

technique SpriteOutline
{
	pass P0
	{
		PixelShader = compile PS_SHADERMODEL MainPS();
	}
};

@Trinith

Thanks for the shader, but the problem is that each character in a drawstring operation gets drawn in a separate draw rectangle. An example of a shader that transforms all transparant pixels to red pixels can be seen below to show this.

I’m also thinking about something like this. I can’t seem to create the desired effect with just a drawstring call and a hlsl shader.

Yea, I figured. That’s a bummer for sure.

It sounds like it’s a performance trade-off between multiple sprite batch renders per text to achieve your effect, or rendering to a full screen render target to do some post processing.

I’m curious as to which ends up performing better!

That’s a bug in your rendering.

You cannot render SDF fonts at integer pixel values, you need floating point accuracy.

Look in SDFFont.cs you have a function to draw a string which uses a glyph batch to render all the chars.

It’s one of the things that caught me out when I first wrote the code (a long time ago :grin:)

Also make sure the sampler states are correct on the font texture, that can make it look awful as well.

@Jelle If you rendered first all the outlines and afterwards the text, would you achieve the desired effect? If so, that’d probably be only 2 drawcalls with SpriteBatch.Deferred

The number of drawstrings is probably not relevant, only the stuff put inside the spritebatch, but I can’t confirm that because DrawString was the first XNA function I replaced with my custom renderer. Hate at first sight :slight_smile:

I’ll try using a render target and post the results here if I can replicate my font outlines.

@KakCAT

@Jelle If you rendered first all the outlines and afterwards the text, would you achieve the desired effect? If so, that’d probably be only 2 drawcalls with SpriteBatch.Deferred

Probably. I’m still looking for an optimal solution :slight_smile:

I’m making progress. :slight_smile:

I’m using a full screen rendertarget like you said. I draw the text to the rendertarget and apply a custom hlsl shader to it.
Haven’t tested the fps on both methods yet.

EDIT, full shader:

I will test the shader against the drawstring method tomorrow. Feel free to ask for the shader if you want, I don’t know if it’s very efficient though.

EDIT 2: Fast test showed that the hlsl shader & full screen rendertarget method is faster. I’ll post more details later.

2 Likes

Hey you, open secret how did you achive such high FPS?

@Rombersoft

I used a small resolution for the shader tests (1100 x 900) and nothing else gets drawn to the screen except the font, shader and tile texture. I usually get around 500 fps when the screen is full of tiles (~33k) on a 3440 x 1440 resolution though.

You can unlock your fps limit with

        graphics.SynchronizeWithVerticalRetrace = false; // vsync
        IsFixedTimeStep = false; // Don't force equal timestep updates

in your Game1 constructor.