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
{
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]
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? :)