Creating a white outline around text texture Open-ES 2.0

Hi, I have just learned how to render text on to the screen. This text is rendered on top of a map. For clarity, what I would like to do is make a white outline or glow (just a couple of pixels wide) around the rendered text.

The fonts come from a ttf file and get transformed into a bitmap texture atlas.

Below is a screen shot: (Bottom left is where the text is)
[ATTACH=CONFIG]529[/ATTACH]

You could first draw the text with the bold form of the font in white and then draw the regular text on top of it. Valve’s technique using distance fields to store the font glyphs (paper) also allows adding an outline in a shader.

Thanks Carsten, I think if I can implement it in the shader I will, however implementing what Valve has done into it has got me a little baffled.

Below is my vertex and fragment shader.

public static String getFontVertexShader()
{
return
"precision highp float;
"
+ "uniform mat4 u_MVPMatrix;
" // An array representing the combined model/view/projection matrices for each sprite

	+ "attribute vec4 a_Position;				

" // Per-vertex position information we will pass in.
+ "attribute vec2 a_TexCoordinate;
" // Per-vertex texture coordinate information we will pass in
+ "varying vec2 v_TexCoordinate;
" // This will be passed into the fragment shader.
+ "void main()
" // The entry point for our vertex shader.
+ "{
"
+ " v_TexCoordinate = a_TexCoordinate;
"
+ " gl_Position = u_MVPMatrix * a_Position;
" // gl_Position is a special variable used to store the final position.
+ "}
";
}

public static String getFontFragmentShader()
{
return
"precision highp float;
" // Set the default precision to medium. We don’t need as high of a precision in the fragment shader.
+ "uniform sampler2D u_Texture;
" // The input texture.
+ "uniform vec4 u_Color;
"
+ "varying vec2 v_TexCoordinate;
" // Interpolated texture coordinate per fragment.

	+ "void main()											

" // The entry point for our fragment shader.
+ "{
"
+ " gl_FragColor = (u_Color * texture2D(u_Texture, v_TexCoordinate).w);
"
+ "}
";
}

Would really appreciate help on this

Hmm, what exactly is your question?
As a simpler alternative and since (if I read it correctly) you are only using the alpha channel of your texture you could implement a simple edge detection in your fragment shader and use a different colour if you detect an edge texel. For the edge detection, in addition to sampling at the fragment’s texture location sample neighbouring texels and determine if the values are “similar enough” - you should be able to find lots of material on this, since it is used for many post processing effects as well as a whole bunch of anti-aliasing techniques.

PS: source code is usually easier to read in a monospace font, [noparse]


[/noparse] tags should take care of that.

Yes I am only using the alpha. Edge detection sounds promising I will check it out.
PSS: Next time round I will do that.

I have found this fragment shader at example:

precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;

void main()
{
vec3 irgb = texture2D(u_Texture, v_TexCoordinate).rgb;
float ResS = 720.;
float ResT = 720.;

vec2 stp0 = vec2(1./ResS, 0.);
vec2 st0p = vec2(0., 1./ResT);
vec2 stpp = vec2(1./ResS, 1./ResT);
vec2 stpm = vec2(1./ResS, -1./ResT);

const vec3 W = vec3(0.2125, 0.7154, 0.0721);

float i00 = dot(texture2D(u_Texture, v_TexCoordinate).rgb, W);
float im1m1 = dot(texture2D(u_Texture, v_TexCoordinate-stpp).rgb, W);
float ip1p1 = dot(texture2D(u_Texture, v_TexCoordinate+stpp).rgb, W);
float im1p1 = dot(texture2D(u_Texture, v_TexCoordinate-stpm).rgb, W);
float ip1m1 = dot(texture2D(u_Texture, v_TexCoordinate+stpm).rgb, W);
float im10 = dot(texture2D(u_Texture, v_TexCoordinate-stp0).rgb, W);
float ip10 = dot(texture2D(u_Texture, v_TexCoordinate+stp0).rgb, W);
float i0m1 = dot(texture2D(u_Texture, v_TexCoordinate-st0p).rgb, W);
float i0p1 = dot(texture2D(u_Texture, v_TexCoordinate+st0p).rgb, W);
float h = -1.*im1p1 - 2.*i0p1 - 1.*ip1p1 + 1.*im1m1 + 2.*i0m1 + 1.*ip1m1;
float v = -1.*im1m1 - 2.*im10 - 1.*im1p1 + 1.*ip1m1 + 2.*ip10 + 1.*ip1p1;
float mag = length(vec2(h, v));
 vec3 target = vec3(mag, mag, mag);
gl_FragColor = vec4(mix(irgb, target, 1.0),1.);
 }

I have altered this so ResS = 17, ResT = 24 for my own texture size.
After testing this however it just displays a black box, I guess this is caused by the fact that it is not using alpha?

My questions are:
What do I need to change to get it to work with alpha.
and
What is the process, do I do two draws first to draw the white edge I am after, then do a second draw for the actual text itself?

If I’m reading it correctly it converts each RGB sample to a luminance value (with the dot product with W). I suppose you could try getting rid of that and just use the alpha component of the texture. What also looks odd is the mix() call at the bottom, since the last parameter is a constant it does not really interpolate between irgb and target, but always uses the value of target.

I have modified things to be a little simpler, or so I thought.
I have tried creating a bitmap with the same fonts, except bold and white. This is going to provide me with a white outline(background) for my black text.

I save out the bitmap to a png file to check that it is correct, it is correct. Below is the screen shot from opening the png in fireworks)
[ATTACH=CONFIG]530[/ATTACH]

Somewhere in between the loading of the texture and the rendering it is sticking in grey on the edges. Below is a screenshot, I have only rendered the white background in the bottom left corner.
[ATTACH=CONFIG]531[/ATTACH]

Below is my texture loader, shader and the third is the draw function which has the blend I’m using.

At the moment I just trying to get the white background rendering correctly.
How do I get rid of the grey?

public static int loadFontTexture(Bitmap bitmap)
{
    final int[] textureHandle = new int[1];
    GLES20.glGenTextures(1, textureHandle, 0);
 
    if (textureHandle[0] != 0)
    {
        // Bind to the texture in OpenGL
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);
 
        // Set filtering
		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

        // Load the bitmap into the bound texture.
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
        bitmap.recycle();
    }
 
    if (textureHandle[0] == 0)
    {
        throw new RuntimeException("Error loading texture.");
    }
    return textureHandle[0];
}
public static String getFontFragmentShader()
{
	return
		"precision highp float;       	  
"	// Set the default precision to medium. We don't need as high of a
												// precision in the fragment shader.
		+ "uniform sampler2D u_Texture;   
"	// The input texture.
		+ "varying vec2 v_TexCoordinate;  
"	// Interpolated texture coordinate per fragment.
		
		+ "void main()                    
"	// The entry point for our fragment shader.
		+ "{                              
"
		+ "   gl_FragColor = (texture2D(u_Texture, v_TexCoordinate));
" // Pass the color directly through the pipeline.
		+ "}                              
";
}
private void drawSystemFontTextures()
{
	GLES20.glUseProgram(mFontTextureProgramHandle);

    mFontMVPMatrixHandle = GLES20.glGetUniformLocation(mFontTextureProgramHandle, "u_MVPMatrix");
	mFontPositionHandle = GLES20.glGetAttribLocation(mFontTextureProgramHandle, "a_Position");
    mFontTextureCoordinateHandle = GLES20.glGetAttribLocation(mFontTextureProgramHandle, "a_TexCoordinate");
	mTextureUniformHandle = GLES20.glGetUniformLocation(mFontTextureProgramHandle, "u_Texture");

    GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
    GLES20.glEnable(GLES20.GL_BLEND);
    
    // Set the active texture unit to texture unit 0.
    GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
    // Bind the texture to this unit.
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, glText.mFontTextureDataHandle);
    // Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.
    GLES20.glUniform1i(mTextureUniformHandle, 2);
    
    GLES20.glUniformMatrix4fv(mFontMVPMatrixHandle, 1, false, mMVPMatrix, 0);
    
	// Pass in the position information
    glText.vertices.position(0);
    GLES20.glVertexAttribPointer(mFontPositionHandle, glText.POSITION_CNT_2D, GLES20.GL_FLOAT, false, glText.VERTEX_SIZE * mBytesPerFloat, glText.vertices);
    GLES20.glEnableVertexAttribArray(mFontPositionHandle);

	// bind texture position pointer
    glText.vertices.position(glText.POSITION_CNT_2D);  // Set Vertex Buffer to Texture Coords (NOTE: position based on whether color is also specified)
	GLES20.glVertexAttribPointer(mFontTextureCoordinateHandle, glText.TEXCOORD_CNT, GLES20.GL_FLOAT, false, glText.VERTEX_SIZE * mBytesPerFloat, glText.vertices);
	GLES20.glEnableVertexAttribArray(mFontTextureCoordinateHandle);

    // Draw the cube.
    glText.indices.position(0);
    GLES20.glDrawElements(GLES20.GL_TRIANGLES, glText.indices.capacity(), GLES20.GL_UNSIGNED_SHORT, glText.indices);
    
    GLES20.glDisable(GLES20.GL_BLEND);
}