Font rendering with BMFont

Hey guys. I’m trying to use BMFont to render some bitmap fonts with my engine and it almost works fine, however, I got stuck with a minor issue that I don’t know how to overcome:


:point:

As you can see some of the fonts are in line but some of them are not.
I know for a fact that this has to do something with the characters’ Y offset, however, if I don’t add that value to my cursor position then 90% of the characters will be in line but some characters such as y, g, and p (so basically those that would need minus offsetting) will be “in line” too, so their descender won’t be placed lower.
I don’t know what I’m messing up so please, if someone has experience with parsing and rendering BMFont bitmaps help me out here. :slight_smile:

Here’s my source code (I can provide the other classes as well, however I think this is the only important part):

package com.pandadev.ogame.fonts;

import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector2f;

import com.pandadev.ogame.MatrixStack;
import com.pandadev.ogame.textures.Texture;

public class FontText {
	public FontText(Font font, String text, Vector2f position){
		this.fontTexture = font.getTexture();
		//Generate text using the font information
		Vector2f cursor = new Vector2f(position);
		FontCharacter lastCharacter = null;
		char[] textCharacters = text.toCharArray();
		FloatBuffer vertexData = BufferUtils.createFloatBuffer(textCharacters.length*16);
		for(char textCharacter : textCharacters){
			//Convert the character from ASCII value to FontCharacter object
			FontCharacter character = font.getCharacter(Character.toString(textCharacter));
			if(character == null){
				character = font.getCharacter(Character.toString('?'));
			}
			Vector2f charPos = new Vector2f(cursor.x+character.getOffsetX(), cursor.y+character.getOffsetY());
			//Check and apply kerning if possible
			if(lastCharacter != null){
				FontKerning kerning = font.getKerning(lastCharacter.getId(), character.getId());
				if(kerning != null)
					charPos.x += kerning.getValue();
			}
			//Calculating vertices and uploading to the buffer
			float[] vertices = {
				charPos.x, charPos.y, //Vertex #0
				character.getTextureX(), character.getTextureEndY(), //TexCoord #0
				charPos.x+character.getWidth(), charPos.y, //Vertex #1
				character.getTextureEndX(), character.getTextureEndY(), //TexCoord #1
				charPos.x+character.getWidth(), charPos.y+character.getHeight(), //Vertex #2
				character.getTextureEndX(), character.getTextureY(), //TexCoord #2
				charPos.x, charPos.y+character.getHeight(),  //Vertex #3
				character.getTextureX(), character.getTextureY() //TexCoord #3
			};
			vertexData.put(vertices);
			cursor.x += character.getAdvanceX();
			lastCharacter = character;
		}
		//Generate indices
		this.index_count = textCharacters.length*6;
		ShortBuffer indexData = BufferUtils.createShortBuffer(index_count);
		short counter = 0;
		for(int i = 0; i < textCharacters.length; i++){
			short[] indices = {
				counter++, counter++, counter++,
				((short)(counter-3)), ((short)(counter-1)), counter++
			};
			indexData.put(indices);
		}
		vertexData.flip();
		indexData.flip();
		//Upload data to the GPU
		this.vao = GL30.glGenVertexArrays();
		this.vbo = GL15.glGenBuffers();
		this.ibo = GL15.glGenBuffers();
		GL30.glBindVertexArray(vao);
			GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
			GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertexData, GL15.GL_STATIC_DRAW);
			GL20.glEnableVertexAttribArray(0);
			GL20.glEnableVertexAttribArray(1);
			GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 16, 0L);
			GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, 16, 8L);
			GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo);
			GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexData, GL15.GL_STATIC_DRAW);
		GL30.glBindVertexArray(0);
	}
	private final int vao;
	private final int vbo, ibo;
	private final int index_count;
	private final Matrix4f textureMatrix = new Matrix4f();
	private final Texture fontTexture;
	
	public Texture getFontTexture(){
		return fontTexture;
	}
	
	public void render(){
		MatrixStack.getTextureMatrix().load(textureMatrix);
		fontTexture.bind();
		GL30.glBindVertexArray(vao);
			GL11.glDrawElements(GL11.GL_TRIANGLES, index_count, GL11.GL_UNSIGNED_SHORT, 0L);
		GL30.glBindVertexArray(0);
		fontTexture.unbind();
	}
	
	public void destroy(){
		GL30.glDeleteVertexArrays(vao);
		GL15.glDeleteBuffers(vbo);
		GL15.glDeleteBuffers(ibo);
	}
	
}

I can’t find anything wrong in your code above, but I think this has to do with how you render the font. Characters that should be aligned with the baseline but ascend above it (t, f, l, any capital letter, etc) are rendered as though they’re characters that descend below it (y, g, p, q, etc) and vice versa. So you just need to “flip” the y-offset for those characters. Hopefully this makes sense. :smiley:

Thank you, you got me started in the right way but that wasn’t quite the solution.

So what the problem was that all the tutorials I found on the subject used (0,0) as their top-left coordinate (but they never really mentioned it), while (0,0) for me is my bottom-left coordinate so all I had to do is instead of rendering from the bottom to the top (BL, BR, UR, UL order) I’m now rendering from top to bottom (TL, BL, BR, TR (these are CCW orders btw)) and instead of adding height to the cursor position I negate it. It’s a bit hard to explain, but you’ll find a picture below from the result and the working source code, feel free to use it for your own projects if needed.


:point:

Source code:

package com.pandadev.ogame.fonts;

import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector2f;

import com.pandadev.ogame.MatrixStack;
import com.pandadev.ogame.textures.Texture;

public class FontText {
	public FontText(Font font, String text, Vector2f position){
		this.fontTexture = font.getTexture();
		//Generate text using the font information
		Vector2f cursor = new Vector2f(position);
		FontCharacter lastCharacter = null;
		char[] textCharacters = text.toCharArray();
		FloatBuffer vertexData = BufferUtils.createFloatBuffer(textCharacters.length*16);
		for(char textCharacter : textCharacters){
			//Convert the character from ASCII value to FontCharacter object
			FontCharacter character = font.getCharacter(Character.toString(textCharacter));
			if(character == null){
				character = font.getCharacter(Character.toString('?'));
			}
			Vector2f charPos = new Vector2f(cursor.x+character.getOffsetX(), cursor.y-character.getOffsetY());
			//Check and apply kerning if possible
			if(lastCharacter != null){
				FontKerning kerning = font.getKerning(lastCharacter.getId(), character.getId());
				if(kerning != null)
					charPos.x += kerning.getValue();
			}
			//Calculating vertices and uploading to the buffer
			float[] vertices = {
				charPos.x, charPos.y, //Vertex #0 - UL
				character.getTextureX(), character.getTextureY(), //TexCoord #0 - UL
				charPos.x, charPos.y-character.getHeight(), //Vertex #1 - BL
				character.getTextureX(), character.getTextureEndY(), //TexCoord #1 - BL
				charPos.x+character.getWidth(), charPos.y-character.getHeight(), //Vertex #2 - BR
				character.getTextureEndX(), character.getTextureEndY(), //TexCoord #2 - BR
				charPos.x+character.getWidth(), charPos.y, //Vertex #3 - UR
				character.getTextureEndX(), character.getTextureY() //TexCoord #3 - UR
			};
			vertexData.put(vertices);
			cursor.x += character.getAdvanceX();
			lastCharacter = character;
		}
		//Generate indices
		this.index_count = textCharacters.length*6;
		ShortBuffer indexData = BufferUtils.createShortBuffer(index_count);
		short counter = 0;
		for(int i = 0; i < textCharacters.length; i++){
			short[] indices = {
				counter++, counter++, counter++,
				((short)(counter-3)), ((short)(counter-1)), counter++
			};
			indexData.put(indices);
		}
		vertexData.flip();
		indexData.flip();
		//Upload data to the GPU
		this.vao = GL30.glGenVertexArrays();
		this.vbo = GL15.glGenBuffers();
		this.ibo = GL15.glGenBuffers();
		GL30.glBindVertexArray(vao);
			GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
			GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertexData, GL15.GL_STATIC_DRAW);
			GL20.glEnableVertexAttribArray(0);
			GL20.glEnableVertexAttribArray(1);
			GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 16, 0L);
			GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, 16, 8L);
			GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo);
			GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexData, GL15.GL_STATIC_DRAW);
		GL30.glBindVertexArray(0);
	}
	private final int vao;
	private final int vbo, ibo;
	private final int index_count;
	private final Matrix4f textureMatrix = new Matrix4f();
	private final Texture fontTexture;
	
	public Texture getFontTexture(){
		return fontTexture;
	}
	
	public void render(){
		MatrixStack.getTextureMatrix().load(textureMatrix);
		fontTexture.bind();
		GL30.glBindVertexArray(vao);
			GL11.glDrawElements(GL11.GL_TRIANGLES, index_count, GL11.GL_UNSIGNED_SHORT, 0L);
		GL30.glBindVertexArray(0);
		fontTexture.unbind();
	}
	
	public void destroy(){
		GL30.glDeleteVertexArrays(vao);
		GL15.glDeleteBuffers(vbo);
		GL15.glDeleteBuffers(ibo);
	}
	
}

I will probably write a complete tutorial on the subject soon, so those who don’t know how to do this or don’t understand it, stay tuned. :wink: