Signed-Distance Field Font

Started by
20 comments, last by Vincent_M 8 years, 11 months ago

Hey guys,

I'm trying to implement SDF font for our game. My images are really wavy, as shown in the screenshot here. Here's the shader I'm using:


// vertex shader
#version 330 core

layout(location = 0) in vec4 in_position;
layout(location = 1) in vec2 in_coord;

out vec2 frag_coord;

uniform mat4 u_pvmMat;

void main()
{
	frag_coord = in_coord;
	gl_Position = u_pvmMat * in_position;
}


// fragment shader
#version 330 core
in vec2 frag_coord;

out vec4 out_color;

uniform vec4 u_color;
uniform sampler2D u_tex;

const float edgeDistance = 0.5;

void main()
{
	float distance = texture(u_tex, frag_coord).r;"
	float edgeWidth = 0.7 * length(vec2(dFdx(distance), dFdy(distance)));
	float opacity = smoothstep(edgeDistance - edgeWidth, edgeDistance + edgeWidth, distance);
	out_color = vec4(u_color.rgb, opacity);
}

I'm using FreeType2 to get the glyph data. I generate my glyph at a size of 24pt with a 4-pixel spread. This means that the glyph's final image size in the texture will have 8 pixels of padding for both dimensions. There's only 1 glyph in my texture atlas, so I just pad that image up to the nearest power of 2.

The way I generate the edge info is by first copying the FT2 bitmap data to the padded image buffer. While doing so, I check to see if the current sample is > 0. If it isn't, it stays as 0. If > 0, I check all adjacent pixels to that sample. If all adjacent pixels are > 0, then the sample is INSIDE the glyph, and gets a value of 255, otherwise, it's an EDGE and gets a value of 127. Next, I calculate the spread on the outside of the glyph. Here's my code for that:


unsigned char topSample = 0;
unsigned char bottomSample = 0;
unsigned char leftSample = 0;
unsigned char rightSample = 0;
	
// perform the first-pass over the glyph buffer to copy the bitmap into the padded glyph buffer
for(int y=0;y<slot->bitmap.rows;++y)
{
	for(int x=0;x<slot->bitmap.width;++x)
	{
		unsigned char sample = slot->bitmap.buffer[(slot->bitmap.width * y) + x];
		if(sample > 0)
		{
			// find adjacent samples
			if(y > 0)
				topSample = slot->bitmap.buffer[(slot->bitmap.width * (y - 1)) + x];
			else
				topSample = 0;
			if(y < slot->bitmap.rows - 1)
				bottomSample = slot->bitmap.buffer[(slot->bitmap.width * (y + 1)) + x];
			else
				bottomSample = 0;
			if(x > 0)
				leftSample = slot->bitmap.buffer[(slot->bitmap.width * y) + (x - 1)];
			else
				leftSample = 0;
			if(x < slot->bitmap.width - 1)
				rightSample = slot->bitmap.buffer[(slot->bitmap.width * y) + (x + 1)];
			else
				rightSample = 0;
				
			// check if an adjacent sample is zero indicating an edge is found
			if(!topSample || !bottomSample || !leftSample || !rightSample)
				glyphBuffer[(imageWidth * (y + spread)) + (x + spread)] = 127;
			else // otherwise, the sample is completely inside the glyph
				glyphBuffer[(imageWidth * (y + spread)) + (x + spread)] = 255;
		} else
			glyphBuffer[(imageWidth * (y + spread)) + (x + spread)] = 0;
	}
}
	
// perform the second-pass over the glyph to calculate the final pixel values
for(int y=0;y<glyphHeight;++y)
{
	for(int x=0;x<glyphWidth;++x)
	{
		// get the sample, and make sure it's not an edge
		unsigned char sample = glyphBuffer[(imageWidth * y) + x];
		if(sample != 127)
		{
			// check if the sample is inside the glyph
			if(sample == 255)
			{
                            // leftover remnant logic...
			} else {
				// otherwise, find the minimum distance to the edge
				unsigned char minDistance = 255;
					
				// loop through all samples in the bitmap again
				for(int y2=0;y2<glyphHeight;++y2)
				{
					for(int x2=0;x2<glyphWidth;++x2)
					{
						// find the test sample, and check if it's an edge
						unsigned char testSample = glyphBuffer[(imageWidth * y2) + x2];
						if(testSample == 127)
						{
							// find the distance from the current sample, and testSample (it's an edge)
							short xDiff = x - x2;
							short yDiff = y - y2;
							unsigned char distance = (unsigned char)sqrtf((xDiff * xDiff) + (yDiff * yDiff));
								
							// update the minDistance if the current distance is less
							if(minDistance > distance)
								minDistance = distance;
						}
					}
				}
					
				// make sure the minDistance isn't beyond the spread
				if(minDistance <= spread)
					glyphBuffer[(imageWidth * y) + x] = (unsigned char)((float)(spread - minDistance + 1) / (float)(spread + 1) * 127.0f);
			}
		}
	}
}
Advertisement

I don't see any screen shot.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

My attachment didn't save, so I uploaded it to Imgur. There's a link to it at the beginning of the post now.


While doing so, I check to see if the current sample is > 0. If it isn't, it stays as 0. If > 0, I check all adjacent pixels to that sample. If all adjacent pixels are > 0, then the sample is INSIDE the glyph, and gets a value of 255, otherwise, it's an EDGE and gets a value of 127.

This doesn't sound right. the edge should be 0.5, but not every pixel in the image is 1.0 (or 255). The min and max distance to an outside pixel will be mapped to 0.5 - 1.0. For example, if you have a text image of half black and half white, the alpha signed -distance values of a single row would look something like:

0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0

It shouldn't be 0.0 0.1 0.2 0.3 0.4 0.5 1.0 1.0 1.0 1.0 1.0 <- is this what you're doing for the "inside" pixels?

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Yeah, the edges are 0.5, and anything inside of that is 1.0. My reason for this is because the glyphs are usually drawn so small that there's maybe 1 pixel between edges in most cases, so I just approximate. The spread, which is set to 4 pixels, get distributed out from 0.5.

Can you post a picture of the signed distance field for the image? That' might help. Also, how big is the source image and how small is the scaled SDF alpha channel?

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Can you post a picture of the signed distance field for the image? That' might help. Also, how big is the source image and how small is the scaled SDF alpha channel?

So this is where I think I'm doing it all wrong. I'm using Freetype2 to generate my glyph's source image. For the letter 'A', Freetype2 gives me a 18x18 pixel image. Then, I run through that image with the code above that'll add padding for the spread, then pad that image to a power of 2 size. I just save it out as an 8-bit buffer to a file, so it might be more convenient to provide you the ASCII version:


   0    0    0    0    0    0    0    0   25   25   25   25   25   25   25   25   25    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0    0    0   25   50   50   50   50   50   50   50   50   50   25    0    0    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0    0   25   50   76   76   76   76   76   76   76   76   76   50   25    0    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0    0   25   50   76  101  101  101  101  101  101  101   76   50   25    0    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0    0   25   50   76  101  128  128  128  128  128  101   76   50   25    0    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0   25   50   76   76  101  128  255  255  255  128  101   76   76   50   25    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0    0   25   50   76  101  101  128  255  255  255  128  101  101   76   50   25    0    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0   25   50   76   76  101  128  255  255  128  255  255  128  101   76   76   50   25    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0   25   50   76  101  101  128  255  128  101  128  255  128  101  101   76   50   25    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0    0   25   50   76  101  128  255  255  128  101  128  255  255  128  101   76   50   25    0    0    0    0    0    0    0    0    0    0    0 
   0    0    0   25   50   76   76  101  128  255  128  101  101  101  128  255  128  101   76   76   50   25    0    0    0    0    0    0    0    0    0    0 
   0    0    0   25   50   76  101  101  128  255  128  101   76  101  128  255  128  101  101   76   50   25    0    0    0    0    0    0    0    0    0    0 
   0    0    0   25   50   76  101  128  255  255  128  101   76  101  128  255  255  128  101   76   50   25    0    0    0    0    0    0    0    0    0    0 
   0    0   25   50   76   76  101  128  255  128  101  101  101  101  101  128  255  128  101   76   76   50   25    0    0    0    0    0    0    0    0    0 
   0    0   25   50   76  101  101  128  255  255  128  128  128  128  128  255  255  128  101  101   76   50   25    0    0    0    0    0    0    0    0    0 
   0   25   50   76   76  101  128  255  255  128  128  128  128  128  128  128  255  255  128  101   76   76   50   25    0    0    0    0    0    0    0    0 
   0   25   50   76  101  101  128  255  128  101  101  101  101  101  101  101  128  255  128  101  101   76   50   25    0    0    0    0    0    0    0    0 
   0   25   50   76  101  128  255  255  128  101   76   76   76   76   76  101  128  255  255  128  101   76   50   25    0    0    0    0    0    0    0    0 
  25   50   76   76  101  128  255  128  101  101   76   50   50   50   76  101  101  128  255  128  101   76   76   50   25    0    0    0    0    0    0    0 
  25   50   76  101  101  128  255  128  101   76   76   50   25   50   76   76  101  128  255  128  101  101   76   50   25    0    0    0    0    0    0    0 
  25   50   76  101  128  128  128  128  101   76   50   25    0   25   50   76  101  128  128  128  128  101   76   50   25    0    0    0    0    0    0    0 
  25   50   76  101  101  101  101  101  101   76   50   25    0   25   50   76  101  101  101  101  101  101   76   50   25    0    0    0    0    0    0    0 
  25   50   76   76   76   76   76   76   76   76   50   25    0   25   50   76   76   76   76   76   76   76   76   50   25    0    0    0    0    0    0    0 
   0   25   50   50   50   50   50   50   50   50   25    0    0    0   25   50   50   50   50   50   50   50   50   25    0    0    0    0    0    0    0    0 
   0    0   25   25   25   25   25   25   25   25    0    0    0    0    0   25   25   25   25   25   25   25   25    0    0    0    0    0    0    0    0    0 
You need to generate the glyph at a really large size, calculate the distance field and then scale down. In Valve's paper I think they do it at 4096x4096 pixels, or something like that. I was having exactly the same problem as you until I generated the glyph at a huge size, performed the distance field calculation at *that* size, and then scaled down. Then suddenly everything started rendering nicely. Note that the edge threshold value still needs to be adjusted depending on the size you render at.

You need to generate the glyph at a really large size, calculate the distance field and then scale down. In Valve's paper I think they do it at 4096x4096 pixels, or something like that. I was having exactly the same problem as you until I generated the glyph at a huge size, performed the distance field calculation at *that* size, and then scaled down. Then suddenly everything started rendering nicely. Note that the edge threshold value still needs to be adjusted depending on the size you render at.

^^^ This. I rendered my fonts to a 4096x4096. Make sure you don't use the freetype antialiasing. You want the monochrome bitmap, with only two values. I think my naive SDF algorithm took about 40 minutes to process the 4096x4096. But it needs to be that size to get the fidelity around the edge. If the source image is too small it doesn't work.

I tried a source image of 796x796, and it was way different.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532


The way I generate the edge info is by first copying the FT2 bitmap data to the padded image buffer. While doing so, I check to see if the current sample is > 0. If it isn't, it stays as 0. If > 0, I check all adjacent pixels to that sample. If all adjacent pixels are > 0, then the sample is INSIDE the glyph, and gets a value of 255, otherwise, it's an EDGE and gets a value of 127. Next, I calculate the spread on the outside of the glyph.

Hmmm.. here is the approach which I use.

1. Your source image has only binary values, that is either 0 or 255.

2. You need to save the closest, normalized, euclidian distance to a pixel of the opposite value.

3. You should use the sign to indicate, if the pixel is inside or outside the source.

4. You should map the normalized distance from -1..0...1 to 0..1

In pseudo code:


int MAX_PIXEL_SEARCH_RADIUS = 64

for Pixel p in sourceimage do
  float newPixelValue;
  // binary only
  if p.value>128 then
     float shortest_distance = MAX_PIXEL_SEARCH_RADIUS;
     // attention: really unoptimized code ahead ;-)
     for Pixel neigbor in sourceimage do
       float d = distance(p,neighbor);
       // only consider opposite colored pixels !
       if neighbor.value<=128 && d<shortest_distance  then
          shortest_distance = d
       end
     end
     newPixelValue = + (shortest_distance/MAX_PIXEL_SEARCH_RADIUS);
  else
     // ... the other way around, use minus to indicate that this pixel is outside
     newPixelValue = - (shortest_distance/MAX_PIXEL_SEARCH_RADIUS);
  end
  // newPixelValue has now a value between -1(completely outside),0(on border),1 (completely inside)
  float mappedValue = clamp(0.0,1.0,newPixelValue *0.5+0.5);

  .. save mapped value to target ..
end


Because of using only binary input data, you need quite large image sources to have smooth edges. The final image can then be scaled down, because the final image is a quite robust data source.

This topic is closed to new replies.

Advertisement