Improved Alpha Magnification 2

Published April 16, 2008
Advertisement
In the spirit of sharing, here's the shader source and the image preprocessing source for the alpha magnification code.

GLSL fragment shader:
uniform sampler2D textMap;void main(){	vec4 baseColour = vec4(0, 0, 1, 1);	baseColour.a = texture2D(textMap, gl_TexCoord[0].xy).a;	const float distanceFactor = baseColour.a;		const float width = fwidth(gl_TexCoord[0].x) * 25.0; // 25 is an arbitrary scale factor to get an appropriate width for antialiasing	baseColour.a = smoothstep(0.5-width, 0.5+width, baseColour.a);		// Outline constants	const vec4 outlineColour = vec4(1, 1, 1, 1);	const float OUTLINE_MIN_0 = 0.4;	const float OUTLINE_MIN_1 = OUTLINE_MIN_0 + width * 2;	const float OUTLINE_MAX_0 = 0.5;	const float OUTLINE_MAX_1 = OUTLINE_MAX_0 + width * 2;		// Outline calculation	if (distanceFactor > OUTLINE_MIN_0 && distanceFactor < OUTLINE_MAX_1)	{		float outlineAlpha;		if (distanceFactor < OUTLINE_MIN_1)			outlineAlpha = smoothstep(OUTLINE_MIN_0, OUTLINE_MIN_1, distanceFactor);		else			outlineAlpha = smoothstep(OUTLINE_MAX_1, OUTLINE_MAX_0, distanceFactor);					baseColour = mix(baseColour, outlineColour, outlineAlpha);	}			// Shadow / glow constants	const vec2 GLOW_UV_OFFSET = vec2(-0.004, -0.004);	const vec3 glowColour = vec3(0, 0, 0);		// Shadow / glow calculation	float glowDistance = texture2D(textMap, gl_TexCoord[0].xy + GLOW_UV_OFFSET).a;	float glowFactor = smoothstep(0.3, 0.5, glowDistance);		baseColour = mix(vec4(glowColour, glowFactor), baseColour, baseColour.a);		gl_FragColor = baseColour;}


Java image preprocessing:
package net.orangytang.evolved.tools.filters;import java.awt.Color;import java.awt.image.BufferedImage;import java.io.File;import java.io.FileFilter;import java.util.ArrayList;import net.orangytang.evolved.tools.FileAttributes;import org.lwjgl.util.Rectangle;public class DistanceFieldFilter implements ImageFilter{	public FileFilter getFileFilter()	{		return new FileFilter()		{			public boolean accept(File pathname)			{				return true;			}		};	}	public ArrayList preProcess(ArrayList loadedImages)	{		return loadedImages;	}		private static float separation(final float x1, final float y1, final float x2, final float y2)	{		final float dx = x1 - x2;		final float dy = y1 - y2;		return (float)Math.sqrt(dx*dx + dy*dy);	}		public BufferedImage process(BufferedImage inImage, FileAttributes attributes, Rectangle rectangle)	{		System.out.println("DistanceFieldFilter.process");				final int scanSize = 200; // controls the size of the affect. Larger numbers will allow larger outline/shadow regions at the expense of precision and longer preprocessing times				final int outWidth = inImage.getWidth() / 32;		final int outHeight = inImage.getHeight() / 32;				BufferedImage outImage = new BufferedImage(outWidth, outHeight, BufferedImage.TYPE_4BYTE_ABGR);		float[][] distances = new float[outImage.getWidth()][outImage.getHeight()];				final int blockWidth = inImage.getWidth() / outImage.getWidth();		final int blockHeight = inImage.getHeight() / outImage.getHeight();				System.out.println("Block size is "+blockWidth+","+blockHeight);				for (int x=0; x		{			for (int y=0; y			{				distances[x][y] = findSignedDistance( (x * blockWidth) + (blockWidth / 2),													  (y * blockHeight) + (blockHeight / 2),													  inImage,													  scanSize, scanSize);			}		}				float max = 0;		for (int x=0; x		{			for (int y=0; y0
].length; y++)
{
final float d = distances[x][y];
if (d != Float.MAX_VALUE && d > max)
max = d;
}
}
float min = 0;
for (int x=0; x {
for (int y=0; y0].length; y++)
{
final float d = distances[x][y];
if (d != Float.MIN_VALUE && d < min)
min = d;
}
}

final float range = max - min;
final float scale = Math.max( Math.abs(min), Math.abs(max) );

System.out.println("Max: "+max+", Min:"+min+", Range:"+range);

for (int x=0; x {
for (int y=0; y0].length; y++)
{
float d = distances[x][y];

if (d == Float.MAX_VALUE)
d = 1.0f;
else if (d == Float.MIN_VALUE)
d = 0.0f;
else
{
d /= scale;
d /= 2;
d += 0.5f;
}

distances[x][y] = d;
}
}

for (int x=0; x {
for (int y=0; y0].length; y++)
{
float d = distances[x][y];
if (d == Float.NaN)
d = 0;

// As greyscale
// outImage.setRGB(x, y, new Color(d, d, d, 1.0f).getRGB());

// As alpha
outImage.setRGB(x, y, new Color(1.0f, 1.0f, 1.0f, d).getRGB());

// As both
// outImage.setRGB(x, y, new Color(d, d, d, d).getRGB());
}
}

return outImage;
}

private static float findSignedDistance(final int pointX, final int pointY, BufferedImage inImage, final int scanWidth, final int scanHeight)
{
Color baseColour = new Color(inImage.getRGB(pointX, pointY) );
final boolean baseIsSolid = baseColour.getRed() > 0;

float closestDistance = Float.MAX_VALUE;
boolean closestValid = false;

final int startX = pointX - (scanWidth / 2);
final int endX = startX + scanWidth;
final int startY = pointY - (scanHeight / 2);
final int endY = startY + scanHeight;

for (int x=startX; x {
if (x < 0 || x >= inImage.getWidth())
continue;

for (int y=startY; y {
if (y < 0 || y >= inImage.getWidth())
continue;

Color c = new Color(inImage.getRGB(x, y));

if (baseIsSolid)
{
if (c.getRed() == 0)
{
final float dist = separation(pointX, pointY, x, y);
if (dist < closestDistance)
{
closestDistance = dist;
closestValid = true;
}
}
}
else
{
if (c.getRed() > 0)
{
final float dist = separation(pointX, pointY, x, y);
if (dist < closestDistance)
{
closestDistance = dist;
closestValid = true;
}
}
}
}
}

if (baseIsSolid)
{
if (closestValid)
return closestDistance;
else
return Float.MAX_VALUE;
}
else
{
if (closestValid)
return -closestDistance;
else
return Float.MIN_VALUE;
}
}
}



Usual disclaimers apply - this is just from a quick weekend's worth of tinkering, so it's somewhat rough and ready. Released under a "do no evil" license - do what you want as long as you're not being evil with it.

Some notes:
There's quite a lot of magic numbers floating around which would be better exposed and parameters for better flexibility. In particular the "scanSize" in the preprocessing controls how much of the source image is scanned for each output pixel in the distance map. Bigger numbers take more time to preprocess and produce distance maps with a greater spread (meaning you can get fatter outlines and softer shadows). In practice I found that 200 worked well for a 1024x1024 input image, if you're using different input image sizes you might want to change that accordingly.

The shader has a lot of magic numbers, quite a few should be moved into uniform variables for better flexibility (such as fill colour, outline colour, width of outline and shadow offset). This should be trivial.

fwidth() is the built-in screen space derivitive, and is used to get consistant antialiased edges regardless of the actual scale the image is drawn at. I found this always returned 0 on my 6600 at work, so this might be better replaced by a uniform variable and setting it to something appropriate depending on the scale of the text.

Similarly, smoothstep() is another built-in GLSL function which returns unimpressive results on my 6600 resulting in lower quality anialiasing. Not sure if this is a hardware thing or just old drivers. Either way, it might be better to replace it with a custom function that just does a linear interpolation (might be faster too).

If anyone uses this it'd be nice if you could drop me a message and let me know. I always like seeing other people's cool screenshots. [grin]
0 likes 12 comments

Comments

Drilian
Wow, somehow I also missed your previous post (with the giant 'a'). I've actually been experimenting with text rendering...my current method is entirely mesh-based, mostly for the size-on-disk advantages (the font data is all of 26K), however there are issues with the edges that are tricky (my current solution is to render the text as a pre-process to a 4x-sized texture then downsample to the actual on-screen size. It's ugly, but the actual text on-screen doesn't change much so I can cache the results).

However, how good would your letter look if it were 16x16 instead of 32x32 (though at a much smaller size, say 100 pixels tall)? Does it still look good, or do the visuals fall apart?

The reason I ask is that if I could get it down to 16x16 (or even, perhaps, 24x24) per character, I could come close enough to the disk size that I'm aiming for, and get the soft-ish edges "for free" (in a shader).

Eep...rambling. Sorry :)

Anyway, how does it look at 16x16 for the character? :)
April 16, 2008 01:31 PM
OrangyTang
26k is pretty good going for an entire font! How many characters does that contain?

I did try with a 16x16 distance map, but found that bits of the glyph started vanishing due to under sampling (like the thin bits at the top and bottom of the 'a'). If you're using a simpler font (ie. no serfs, and no subtle variation in stroke width) then you might be able to get away with it.

On the other hand, if you're doing a geometry-based approach, have you seen this? The idea is to convert each glyph into a set of bezier curves and then uses a fancy pixel shader to draw them with anti-aliasing. If you're worried about disk space then that might be even smaller than both approaches.
April 16, 2008 02:16 PM
OrangyTang
Ok, so here's one based on a 16x16 distance map:



Better than I remember it actually (I think I had some bugs in my first version of the image filter which caused loss of quality). You can see that it's starting to get a little shakey at the thinner bits but it generally holds it's shape quite well. I had to increase the threshold value a bit to .45 instead of .5 too, so it's slightly thicker than it should be.
April 16, 2008 02:34 PM
Drilian
Quote:Original post by OrangyTang
26k is pretty good going for an entire font! How many characters does that contain?

I did try with a 16x16 distance map, but found that bits of the glyph started vanishing due to under sampling (like the thin bits at the top and bottom of the 'a'). If you're using a simpler font (ie. no serfs, and no subtle variation in stroke width) then you might be able to get away with it.

On the other hand, if you're doing a geometry-based approach, have you seen this? The idea is to convert each glyph into a set of bezier curves and then uses a fancy pixel shader to draw them with anti-aliasing. If you're worried about disk space then that might be even smaller than both approaches.


It's 96 characters (space doesn't count, characters 33-126, then two special characters (the spaceship silhouette and the 'x' for the life counter).

That's an interesting paper (that I just breezed through, I'll have to give it a serious read when I get home from work).

It's a clever idea, though, using a pixel shader to draw the anti-aliasing. I wonder if I could rig up something simple that does that just as well :)

And that 16x16 doesn't look bad, I may have to investigate the Valve method further, as well. :)
April 16, 2008 03:40 PM
Ysaneya
Quote:Original post by OrangyTang
Ok, so here's one based on a 16x16 distance map:




Can you show the same with a 8x8, then 4x4, then 2x2 map ?

Of course, I don't expect the 2x2 to look good, but I find it interesting that the 16x16 looks okay at resolutions a bit higher.

Maybe the 4x4 would look okay when displayed at 16x16 ?
April 18, 2008 03:50 PM
chrisward81
What render states does this technique need?

I'm guessing its:

Alpha Test: On
Threshold: 0.5f
Alpha Test Func: GREATER_EQUAL

Is that correct?

Chris
April 20, 2008 12:50 PM
OrangyTang
Quote:Original post by chrisward81
What render states does this technique need?

I'm guessing its:

Alpha Test: On
Threshold: 0.5f
Alpha Test Func: GREATER_EQUAL

Is that correct?

Chris

For the fixed-function fallback path then that's what you need. For the shader approach just use regular blending (since the shader outputs correct alpha values).

April 21, 2008 03:34 AM
chrisward81
So which states are you setting exactly? And what blending modes, thresholds etc?

Chris.
April 21, 2008 06:40 AM
OrangyTang
Quote:Original post by chrisward81
So which states are you setting exactly? And what blending modes, thresholds etc?

Chris.

For the shader path it's:

glDisable(GL_ALPHA_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

ie. standard blended geometry with no alpha test.
April 22, 2008 05:29 PM
lonesock
Looks great!

I'm writing a tool to build bitmap fonts for use with this very method. Instead of starting with a very high res version explicitly, you start by specifying the texture dimensions. The app then figures out the maximum pixel size per glyph to pack into the requested texture. Then, per-glyph, it uses FreeType2 to render a much higher res glyph (16x higher res), compute the signed distance field for that single glyph, then packs it into the texture.

The thread is here, and any feature requests are gladly accepted.
April 24, 2008 01:43 AM
NineYearCycle
I've put a version that I knocked up over a couple of lunchtimes here (.zip & VC2005 .sln) there's no prizes for guessing where I grabbed the basecode from :D

I'll try and pull out some of the shader constants later on and get it to give some options about which font it loads and uses.

At the moment there's some graphical issues relating to texture wrap, or maybe I've calculated the S & T values wrong for some of the characters. Haven't had chance to take a closer look yet.

This code should be described as having being written using a code-cannon and a thousand retarded monkeys with lump hammers btw.

It uses as input the png font built using Lonesocks SDFont tool and the txt file describing the layout within it.

Space bar to increment through the characters ;) the zipped version I've uploaded has 3 256x256 pngs though as said above its currently hardcoded which one gets loaded... yes _IM_ that lazy that I couldn't be bothered with even a command line option ;)

Anyway there ya go, I know you've obviously already got a working version of your own but thought you'd like to know what others are doing with your implementation.

Thanks very much! I had a blast throwing this together! :-D

Andy
April 26, 2008 10:28 AM
NineYearCycle
Oh I forgot to say, I used Glee for the opengl extension support and that it, totally arbitrarily, demands that your gfx card support GL version 2.0 or better.

Think I did that just because I was testing what version of OpenGL I had on my ATi at work :/ should probably remove it but if it bombs out early that'll be why!

Andy
April 26, 2008 10:33 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement