2D outline pixel shader on a pixel font

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.

3 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.

1 Like

I tested the current shader performance with a bunch of test strings.
The performance of the pixel shader doesnā€™t really drop much with more strings because the shader already checks the whole screen on a downscaled rendertarget.

Shader method: ~ 400 - 500 fps

Drawstring method: ~ 100 - 200 fps

There are probably some improvements I can make but Iā€™m already happy with the results.

2 Likes

Fun side effect of rendering to a smaller render target (and drawing with a higher scale): All custom text effects are now pixel perfect, example:

(green wiggle effect)

4 Likes

Hey @Jelle ! Iā€™ve been using Monogame for a couple months now and have been trying to achieve something like this for awhile and yours looks exactly like what Iā€™m going for! I"m so glad I searched and found your post, and that itā€™s so recent! I feel like I end up finding posts from 2013 or old XNA stuff from 2006 so often haha.

You mentioned you were willing to share this shader. Would you mind sending it to me?

1 Like

Hi @lentil

You can find the current shader here: hlsl shader optimization
The shader is less flexible now, but probably a bit faster. If you need more help, do not hesitate to contact me! :slight_smile:

1 Like

Hi @Jelle

Also getting started and this is pretty cool is the wiggle also done with shaders? Do you have an example I could see?

Thanks!

Hi @brandc87

The character effects like color, wiggle, ā€¦ are all done using C# code. I made custom text rendering classes that deconstruct and render strings.

Example:
Hello Ā§bWorld, Ā§r1red wiggle test

Is a string that I need to render. This string will be split into a list of strings: [Hello, World, red wiggle text]. I use ā€˜Ā§ā€™ as a special character that announces a color for a part of the string, or a color and an effect.

I then calculate the character/string positions depending on the effect and draw them. The wiggle effect splits a string into characters and changes each Y value of each character according to a sinus value, bound to a timer.

I end up with fully animated/colored strings at the end of this process. I then draw all the text to a render target and apply the pixel shader for the extra outlines.

2 Likes