Signed Distance Field Text Rendering: some questions

Started by
9 comments, last by xycsoscyx 8 years, 2 months ago

Hello everyone smile.png

I've just implemented signed distance field text rendering and I've got a few minor problems, I can't solve by myself. wacko.png

First of all, a quick overview of the problems:

  • I advance the cursor on the x-axis with the value of "xAdvance" (should be self-explanatory), but the space between two characters seems to be wrong (see screenshots). How can I fix this?
  • How could I set a font size like "Set this font to a size of 12px"? => How do I calculate the correct box size for a single character?
  • It occurs that the visual font thickness depends on the background color (see screenshots, in the upper one, the font seems to be clearly thicker). Why does it happen? (Due to alpha?) And how can I prevent this behaviour?

So, here are some screenshots:

tQjtDDi.png

5YqB0wp.png

Now, I hope you see what I meant. If not feel free to ask and I will try to clarify it.

Here, my implementation:

The signed distance field font files were created with Hiero

It created the texture (512x512 in my case) and a .fnt text file.

The important (and almost all) parts (/lines) of this file are like


char id=33    x=484  y=202  width=24   height=60   xoffset=-7   yoffset=7    xadvance=33

where x,y,width,height represent non normalised texture coordinates and the offsets and xadvance represent pixel numbers.

The fragment shader, which calculates the actual visual character on the signed distance field texture:


#version 330 core

in vec2 _uv;

uniform vec3 color;
uniform sampler2D fontAtlas;

const float width = 0.5;
const float edge = 0.1;

void main()
{
	float distance = 1.0 - texture(fontAtlas, _uv).a;
	float alpha = 1.0 - smoothstep(width, width + edge, distance);
	gl_FragColor = vec4(color, alpha);
}

This is how I calculate the vertices for drawing:


void Font::write(const char* text, float scale, float x, float y, float z)
{
	for (const char* p = text; *p; p++)
	{

		Glyph g = m_glyphs[*p];

		float gx = x + static_cast<float>(g.xOffset) * scale;
		float gy = y - static_cast<float>(g.yOffset) * scale;
		float gw = static_cast<float>(g.width) * scale;
		float gh = static_cast<float>(g.height) * scale;

		x += g.xAdvance * scale;

		if (!gw || !gh)
			continue;

		float u1 = static_cast<float>(g.x) / static_cast<float>(m_atlas.width);
		float u2 = u1 + (static_cast<float>(g.width) / static_cast<float>(m_atlas.width));
		float v1 = static_cast<float>(m_atlas.height - g.y) / static_cast<float>(m_atlas.height);
		float v2 = v1 - (static_cast<float>(g.height) / static_cast<float>(m_atlas.height));

		m_buffer.push_back(Vertex(gx	 , gy	  , z, u1, v1));
		m_buffer.push_back(Vertex(gx	 , gy - gh, z, u1, v2));
		m_buffer.push_back(Vertex(gx + gw, gy - gh, z, u2, v2));
		m_buffer.push_back(Vertex(gx	 , gy	  , z, u1, v1));
		m_buffer.push_back(Vertex(gx + gw, gy - gh, z, u2, v2));
		m_buffer.push_back(Vertex(gx + gw, gy	  , z, u2, v1));
	}
}

This is how I render all of this:


void Font::render(const glm::mat4& ortho, const glm::vec3& color /*= glm::vec3(1.0f)*/)
{
	if (m_buffer.size() > 0)
	{
		m_shader.bind();
		m_shader.setUniform("ortho", ortho);
		m_shader.setUniform("color", color);

		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, m_atlas.id);
		glBindVertexArray(m_vao);
		glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
		glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * m_buffer.size(), &m_buffer[0], GL_STREAM_DRAW);

		glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(m_buffer.size()));

		glBindTexture(GL_TEXTURE_2D, 0);
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindVertexArray(0);
		glDisable(GL_BLEND);
			
		m_shader.unbind();
		m_buffer.clear();
	}
}

If you need some other code, just say it and I'll provide it. smile.png

Cheers,

NightDreamer

EDIT:

- minor changes in question 1 and 3

Advertisement
  • I advance the cursor on the x-axis with the value of "xAdvance" (should be self-explanatory), but the space between two characters seems to be wrong (see screenshots). How can I fix this?

When you create signed distance field fonts, there's an extra amount of fuzziness around each character. It looks like in the hiero gui it's defined as 'spread' and you need to add an amount of padding to match the spread value. You don't mention that you're setting spread or padding in your config file, so I presume it's just taking a default value. Basically, you need to remove whatever spread/padding value that's being added from the xadvance to sort out the spacing.

  • It occurs that the visual font thickness depends on the background color (see screenshots, in the upper one, the font seems to be clearly thicker). Why does it happen? (Due to alpha?) And how can I prevent this behaviour?

I think that might just be an optical illusion. Some sort of drop shadow (easily implemented by rendering a dark, offset version of the text before the light text), or an outline/outer glow (best implemented in the fragment shader - it's one of the strengths of SDFs) would probably sort it out.

Hello C0lumbo,

thanks for replying. smile.png

  • I advance the cursor on the x-axis with the value of "xAdvance" (should be self-explanatory), but the space between two characters seems to be wrong (see screenshots). How can I fix this?

When you create signed distance field fonts, there's an extra amount of fuzziness around each character. It looks like in the hiero gui it's defined as 'spread' and you need to add an amount of padding to match the spread value. You don't mention that you're setting spread or padding in your config file, so I presume it's just taking a default value. Basically, you need to remove whatever spread/padding value that's being added from the xadvance to sort out the spacing.

Thanks for that. If I substract the spread from xAdvance it seems to be alright.

Anyway, sometimes it looks not so perfect, but I think, this is due to no kerning yet.

Any idea on how to implement it? I've got the data for a simple alphabet (something around 1111 kerning values), but don't know how to iterate it fast enough on a per character basis.

  • It occurs that the visual font thickness depends on the background color (see screenshots, in the upper one, the font seems to be clearly thicker). Why does it happen? (Due to alpha?) And how can I prevent this behaviour?

I think that might just be an optical illusion. Some sort of drop shadow (easily implemented by rendering a dark, offset version of the text before the light text), or an outline/outer glow (best implemented in the fragment shader - it's one of the strengths of SDFs) would probably sort it out.

Unfortunately it is no illusion. rolleyes.gif

Here some screenshot, zoomed in a little bit:

fGeL1Z6.png?1

It is the same font with the same size but without a different color in the background.

Why does it occur? Is my fragment shader wrong?

Thanks and a good evening,

NightDreamer

One of the gotchas with signed distance field fonts is the glyph rendering coordinate system.

The coordinates have to be floats.

Rendering a glyph at 10,10 does not produce the same results as 10.1f, 10.0f

Make sure your xAdvance is a float and you never truncate the coordinates to ints.

Hello Stainless,

that doesn't changed anything and wasn't a problem. smile.png

Anyway, two of the three questions aren't answered by now. wink.png

Have you tried using alpha testing instead of alpha blending? It looks like there is still light red around the second image (the white background), it's just less noticeable because the background is lighter. If you're using bilinear filtering on the font, then the alpha will get softened out around the edges of the glyph, so along with the blending will produce what you're seeing. If you use alpha testing, it will create a hard edge instead based on some absolute value. Using point sampling would probably have the same result, but alpha testing would let you control the glyph size by increasing/decreasing the alpha cutoff (since with bilinear filtering, the edges around the glyph will blend from 0-1).

Helloy xycsoscyx,

that's a point I've forgot to mention:

since I'm using OpenGL 3.3 Core, I can't use alpha testing by using glEnable(GL_ALPHA) and glAlphaFunc(...).

Is there some other way for that?

Are you using pixel shaders for the text rendering? It seems like the only way now is to use the discard operation if the alpha is below a threshold.

http://stackoverflow.com/questions/24302152/opengl-alpha-test-how-to-replace-alphafunc-deprecated

Thanks for that, xycsoscyx.

But it occurs, that if I use alpha testing and discard it, the smooth outline of signed distance field won't be smooth anymore, due to the fact, that the smoothness (like antialiasing) relies on the alpha fading.

Any other ideas?

Discard is the thing to use.

The thing to remember is the texture sampler is handling the blending for you.

Say you have two pixels side by side in your font. One is black and one is white.

When they get scaled to 4 pixels you will have 4 alpha values 0,0f.33f,0.66f,1.0f (at a guess, depends on a lot of things, but it illustrates what I mean)

So when you do this in your pixel shader.

     if (alpha<0.5)
        discard;

You are doing the sharp edge you want.

This topic is closed to new replies.

Advertisement