[Solved] Rendering text in lwjgl without slick

@minigame

You are not supposed to be drawing 50 strings each frame. You are supposed to render text to a texture which you draw every frame and only render text to textures again once the text changes.

Oh, right. Well I implemented a TrueTypeFont class and GLFont for managing custom font instances and drawing them. With a few “logical fonts” integrated into the class.

It’s not the best, but it’s pretty efficient. Not a single loss of frame count, even when drawing 500+ strings per frame.

Source:


package com.javarpg.twod.font;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;

/**
 * A TrueType font implementation originally for Slick, edited for Bobjob's
 * Engine by David Aaron Muhar (bobjob), then edited by Yousef Amar
 * (Paraknight).
 * 
 * Further modified on 2/11/2015 for use with the Two-D lwjgl 2D wrapper, by Minigame.
 * 
 * @author James Chambers (Jimmy)
 * @author Jeremy Adams (elias4444)
 * @author Kevin Glass (kevglass)
 * @author Peter Korzuszek (genail)
 * @author David Aaron Muhar (bobjob)
 * @author Yousef Amar (Paraknight)
 * @author Corey K****** (Minigame)
 */
public class TrueTypeFont {
	
	public final static int ALIGN_LEFT = 0, ALIGN_RIGHT = 1, ALIGN_CENTER = 2;
	/** Array that holds necessary information about the font characters */
	private IntObject[] charArray = new IntObject[256];

	/** Map of user defined font characters (Character <-> IntObject) */
	private Map<Character, IntObject> customChars = new HashMap<Character, IntObject>();

	/** Boolean flag on whether AntiAliasing is enabled or not */
	private boolean antiAlias;

	/** Font's size */
	private int fontSize = 0;

	/** Font's height */
	private int fontHeight = 0;

	/** Texture used to cache the font 0-255 characters */
	private int fontTextureID;

	/** Default font texture width */
	private int textureWidth = 512;

	/** Default font texture height */
	private int textureHeight = 512;

	/** A reference to Java's AWT Font that we create our font texture from */
	private Font font;

	/** The font metrics for our Java AWT font */
	private FontMetrics fontMetrics;

	private int correctL = 9, correctR = 8;

	// TODO : create a texture cache in GLFont specifically for font textures!
	// we will need to enable alpha for the texture instance, so that we can draw Strings
	// without having to call gl blend/alpha settings locally in the TTF class.
	// until that is done, there could be compatibility issues with games powered by 
	// Two-D, or possible unnecessary overhead due to settings not being disabled
	private static AtomicInteger fontId = new AtomicInteger(0);

	private class IntObject {
		/** Character's width */
		public int width;

		/** Character's height */
		public int height;

		/** Character's stored x position */
		public int storedX;

		/** Character's stored y position */
		public int storedY;
	}

	public TrueTypeFont(Font font, boolean antiAlias, char[] additionalChars) {
		this.font = font;
		this.fontSize = font.getSize() + 3;
		this.antiAlias = antiAlias;

		createSet(additionalChars);

		fontHeight -= 1;
		if (fontHeight <= 0)
			fontHeight = 1;
	}

	public TrueTypeFont(Font font, boolean antiAlias) {
		this(font, antiAlias, null);
	}

	public void setCorrection(boolean on) {
		if (on) {
			correctL = 2;
			correctR = 1;
		} else {
			correctL = 0;
			correctR = 0;
		}
	}

	private BufferedImage getFontImage(char ch) {
		// Create a temporary image to extract the character's size
		BufferedImage tempfontImage = new BufferedImage(1, 1,
				BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = (Graphics2D) tempfontImage.getGraphics();
		if (antiAlias == true) {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
		}
		g.setFont(font);
		fontMetrics = g.getFontMetrics();
		int charwidth = fontMetrics.charWidth(ch) + 8;

		if (charwidth <= 0) {
			charwidth = 7;
		}
		int charheight = fontMetrics.getHeight() + 3;
		if (charheight <= 0) {
			charheight = fontSize;
		}

		// Create another image holding the character we are creating
		BufferedImage fontImage;
		fontImage = new BufferedImage(charwidth, charheight,
				BufferedImage.TYPE_INT_ARGB);
		Graphics2D gt = (Graphics2D) fontImage.getGraphics();
		if (antiAlias == true) {
			gt.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		}
		gt.setFont(font);
		gt.setColor(Color.WHITE);
		int charx = 3;
		int chary = 1;
		gt.drawString(String.valueOf(ch), (charx), (chary) + fontMetrics.getAscent());
		return fontImage;

	}

	private void createSet(char[] customCharsArray) {
		// If there are custom chars then I expand the font texture twice
		if (customCharsArray != null && customCharsArray.length > 0) {
			textureWidth *= 2;
		}

		// In any case this should be done in other way. Texture with size
		// 512x512
		// can maintain only 256 characters with resolution of 32x32. The
		// texture
		// size should be calculated dynamicaly by looking at character sizes.

		try {

			BufferedImage imgTemp = new BufferedImage(textureWidth, textureHeight, BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = (Graphics2D) imgTemp.getGraphics();

			g.setColor(new Color(0, 0, 0, 1));
			g.fillRect(0, 0, textureWidth, textureHeight);

			int rowHeight = 0;
			int positionX = 0;
			int positionY = 0;

			int customCharsLength = (customCharsArray != null) ? customCharsArray.length : 0;

			for (int i = 0; i < 256 + customCharsLength; i++) {

				// get 0-255 characters and then custom characters
				char ch = (i < 256) ? (char) i : customCharsArray[i - 256];

				BufferedImage fontImage = getFontImage(ch);

				IntObject newIntObject = new IntObject();

				newIntObject.width = fontImage.getWidth();
				newIntObject.height = fontImage.getHeight();

				if (positionX + newIntObject.width >= textureWidth) {
					positionX = 0;
					positionY += rowHeight;
					rowHeight = 0;
				}

				newIntObject.storedX = positionX;
				newIntObject.storedY = positionY;

				if (newIntObject.height > fontHeight) {
					fontHeight = newIntObject.height;
				}

				if (newIntObject.height > rowHeight) {
					rowHeight = newIntObject.height;
				}

				// Draw it here
				g.drawImage(fontImage, positionX, positionY, null);

				positionX += newIntObject.width;

				if (i < 256) { // standard characters
					charArray[i] = newIntObject;
				} else { // custom characters
					customChars.put(new Character(ch), newIntObject);
				}

				fontImage = null;
			}

			fontTextureID = loadImage(imgTemp);

			// .getTexture(font.toString(), imgTemp);

		} catch (Exception e) {
			System.err.println("Failed to create font.");
			e.printStackTrace();
		}
	}

	private void drawQuad(float drawX, float drawY, float drawX2, float drawY2, float srcX, float srcY, float srcX2, float srcY2) {
		float DrawWidth = drawX2 - drawX;
		float DrawHeight = drawY2 - drawY;
		float TextureSrcX = srcX / textureWidth;
		float TextureSrcY = srcY / textureHeight;
		float SrcWidth = srcX2 - srcX;
		float SrcHeight = srcY2 - srcY;
		float RenderWidth = (SrcWidth / textureWidth);
		float RenderHeight = (SrcHeight / textureHeight);

		GL11.glTexCoord2f(TextureSrcX, TextureSrcY);
		GL11.glVertex2f(drawX, drawY + DrawHeight);
		GL11.glTexCoord2f(TextureSrcX + RenderWidth, TextureSrcY);
		GL11.glVertex2f(drawX + DrawWidth, drawY + DrawHeight);
		GL11.glTexCoord2f(TextureSrcX + RenderWidth, TextureSrcY + RenderHeight);
		GL11.glVertex2f(drawX + DrawWidth, drawY);
		GL11.glTexCoord2f(TextureSrcX, TextureSrcY + RenderHeight);
		GL11.glVertex2f(drawX, drawY);
	}

	public int getWidth(String whatchars) {
		int totalwidth = 0;
		IntObject intObject = null;
		int currentChar = 0;
		for (int i = 0; i < whatchars.length(); i++) {
			currentChar = whatchars.charAt(i);
			if (currentChar < 256) {
				intObject = charArray[currentChar];
			} else {
				intObject = (IntObject) customChars.get(new Character((char) currentChar));
			}
			if (intObject != null) {
				totalwidth += intObject.width;
			}
		}
		return totalwidth;
	}

	public int getHeight() {
		return fontHeight;
	}

	public int getHeight(String HeightString) {
		return fontHeight;
	}

	public int getLineHeight() {
		return fontHeight;
	}

	public void drawString(float x, float y, String whatchars, float scaleX, float scaleY) {
		drawString(x, y, whatchars, 0, whatchars.length() - 1, scaleX, scaleY, ALIGN_LEFT);
	}

	public void drawString(float x, float y, String whatchars, float scaleX, float scaleY, int format) {
		drawString(x, y, whatchars, 0, whatchars.length() - 1, scaleX, scaleY, format);
	}

	public void drawString(float x, float y, String whatchars, int startIndex, int endIndex, float scaleX, float scaleY, int format) {
		IntObject intObject = null;
		int charCurrent;
		int totalwidth = 0;
		int i = startIndex, d, c;
		float startY = 0;

		switch (format) {
		case ALIGN_RIGHT: {
			d = -1;
			c = correctR;
			while (i < endIndex) {
				if (whatchars.charAt(i) == '\n') {
					startY -= fontHeight;
				}
				i++;
			}
		}
		case ALIGN_CENTER: {
			for (int l = startIndex; l <= endIndex; l++) {
				charCurrent = whatchars.charAt(l);
				if (charCurrent == '\n') {
					break;
				}
				if (charCurrent < 256) {
					intObject = charArray[charCurrent];
				} else {
					intObject = (IntObject) customChars.get(new Character((char) charCurrent));
				}
				totalwidth += intObject.width - correctL;
			}
			totalwidth /= -2;
		}
		case ALIGN_LEFT:
			default:
				d = 1;
				c = correctL;
				break;
		}

		GL11.glEnable(GL11.GL_TEXTURE_2D);
		GL11.glEnable(GL11.GL_BLEND);
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, fontTextureID);
		//TextureManager.getInstance().getTextureById(fontTextureID).bind();
		GL11.glBegin(GL11.GL_QUADS);

		while (i >= startIndex && i <= endIndex) {
			charCurrent = whatchars.charAt(i);
			if (charCurrent < 256) {
				intObject = charArray[charCurrent];
			} else {
				intObject = (IntObject) customChars.get(new Character((char) charCurrent));
			}
			if (intObject != null) {
				if (d < 0)
					totalwidth += (intObject.width - c) * d;
				if (charCurrent == '\n') {
					startY -= fontHeight * d;
					totalwidth = 0;
					if (format == ALIGN_CENTER) {
						for (int l = i + 1; l <= endIndex; l++) {
							charCurrent = whatchars.charAt(l);
							if (charCurrent == '\n')
								break;
							if (charCurrent < 256) {
								intObject = charArray[charCurrent];
							} else {
								intObject = (IntObject) customChars
										.get(new Character((char) charCurrent));
							}
							totalwidth += intObject.width - correctL;
						}
						totalwidth /= -2;
					}
					// if center get next lines total width/2;
				} else {
					drawQuad((totalwidth + intObject.width) * scaleX + x,
							startY * scaleY + y, totalwidth * scaleX + x,
							(startY + intObject.height) * scaleY + y,
							intObject.storedX + intObject.width,
							intObject.storedY + intObject.height,
							intObject.storedX, intObject.storedY);
					if (d > 0)
						totalwidth += (intObject.width - c) * d;
				}
				i += d;

			}
		}
		GL11.glEnd();
		//GL11.glDisable(GL11.GL_BLEND);
		//GL11.glDisable(GL11.GL_TEXTURE_2D);
	}

	public static int loadImage(BufferedImage bufferedImage) {
		// TODO : store the bufferedimage as a com.javarpg.twod.Texture instance
		// in the GLFont class. we need to keep track of font textures,
		// but cannot store them locally in the TTF class..
		try {
			short width = (short) bufferedImage.getWidth();
			short height = (short) bufferedImage.getHeight();
			// textureLoader.bpp = bufferedImage.getColorModel().hasAlpha() ?
			// (byte)32 : (byte)24;
			int bpp = (byte) bufferedImage.getColorModel().getPixelSize();
			ByteBuffer byteBuffer;
			DataBuffer db = bufferedImage.getData().getDataBuffer();
			if (db instanceof DataBufferInt) {
				int intI[] = ((DataBufferInt) (bufferedImage.getData().getDataBuffer())).getData();
				byte newI[] = new byte[intI.length * 4];
				for (int i = 0; i < intI.length; i++) {
					byte b[] = intToByteArray(intI[i]);
					int newIndex = i * 4;
					newI[newIndex] = b[1];
					newI[newIndex + 1] = b[2];
					newI[newIndex + 2] = b[3];
					newI[newIndex + 3] = b[0];
				}
				byteBuffer = ByteBuffer.allocateDirect(width * height * (bpp / 8)).order(ByteOrder.nativeOrder()).put(newI);
			} else {
				byteBuffer = ByteBuffer.allocateDirect(width * height * (bpp / 8)).order(ByteOrder.nativeOrder()).put(((DataBufferByte) (bufferedImage.getData() .getDataBuffer())).getData());
			}
			byteBuffer.flip();

			int internalFormat = GL11.GL_RGBA8, format = GL11.GL_RGBA;
			IntBuffer textureId = BufferUtils.createIntBuffer(1);
			
			GL11.glGenTextures(textureId);
			GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId.get(0));

			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP);

			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);

			GL11.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);

			GLU.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D, internalFormat, width, height, format, GL11.GL_UNSIGNED_BYTE, byteBuffer);
			return textureId.get(0);

		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
		return -1;
	}

	public static boolean isSupported(String fontname) {
		Font font[] = getFonts();
		for (int i = font.length - 1; i >= 0; i--) {
			if (font[i].getName().equalsIgnoreCase(fontname)) {
				return true;
			}
		}
		return false;
	}

	public static Font[] getFonts() {
		return GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
	}

	public static byte[] intToByteArray(int value) {
		return new byte[] { (byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) value };
	}

	public void destroy() {
		IntBuffer scratch = BufferUtils.createIntBuffer(1);
		scratch.put(0, fontTextureID);
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
		GL11.glDeleteTextures(scratch);
	}
}


package com.javarpg.twod.font;

import java.awt.Font;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import com.javarpg.twod.texture.Texture;

public class GLFont {

	private static final GLFont INSTANCE = new GLFont();

	/*
	 * A list of "logical fonts", based on an article from the oracle website.
	 */
	private final TrueTypeFont logicalFonts[] = new TrueTypeFont[5];
	private final String fontNames[] = { "Dialog", "DialogInput", "Monospaced", "Serif", "SansSerif" };

	/*
	 * A list of custom fonts which the Two-D developer can implement.
	 */
	private Map<String, TrueTypeFont> customFonts = new HashMap<String, TrueTypeFont>();

	// TODO store a texture instance of the custom fonts, instead of using a seperate system.
	// If you are using slick2D, just ignore this. I use my own texture/texturemanager classes
	// as part of the learning process for me is to not use slick2d
	private List<Texture> fontTextures = new CopyOnWriteArrayList<Texture>();
	
	public enum FontType {
		Dialog, DialogInput, Monospaced, Serif, SansSerif
	}

	public GLFont() {
		for (int i = 0; i < logicalFonts.length; i++) {
			logicalFonts[i] = new TrueTypeFont(new Font(fontNames[i], Font.PLAIN, 12), true);
		}
		
		// just testing custom fonts
		addCustomFont("Arial", false, 12, true);
		addCustomFont("Helvetica", false, 12, true);
	}

	/**
	 * Draws a default logicalFont. The list includes a small array of Two-D integrated fonts, see the {@link #GLFont.FontType} enum
	 * to see which fonts are available by default. Note Arail and Helvetica and be drawn using drawCustomString (as they are
	 * both integrated custom fonts, but not listed as a logic font or part of the FonType enum)
	 * 
	 * @param string The string to be rendered.
	 * @param x The x coordinate in 2D space.
	 * @param y The y coordinate in 2D space.
	 * @param scaleX The scale (size) you want to use while drawing the string.
	 * @param scaleY The scale (size) you want to use while drawing the string.
	 * @param format The string format. 0 = align left, 1 = align right, 2 = align center
	 * @param customFont The name of the custom font. This would have been set when adding a
	 * new font using {@link #addCustomFont(String, boolean, int, boolean)}
	 * @param fontType See {@link #GLFont.FontType} enum for a list of default integrated logical fonts.
	 */
	public void drawString(String string, int x, int y, int scaleX, int scaleY, int format, FontType fontType) {
		switch (fontType) {
		case Dialog:
			logicalFonts[0].drawString(x, y, string, scaleX, scaleY, format);
			break;
		case DialogInput:
			logicalFonts[1].drawString(x, y, string, scaleX, scaleY, format);
			break;
		case Monospaced:
			logicalFonts[2].drawString(x, y, string, scaleX, scaleY, format);
			break;
		case Serif:
			logicalFonts[3].drawString(x, y, string, scaleX, scaleY, format);
			break;
		case SansSerif:
			logicalFonts[4].drawString(x, y, string, scaleX, scaleY, format);
			break;
		default:
			System.err.println("Unknown font type selected!");
			System.exit(1);
			break;
		}
	}

	/**
	 * Adds a custom font to memory using the given variables. This essentially just creates a java.awt.Font
	 * and converts it to a TrueFontType instance for lwjgl display.
	 * 
	 * @param fontName The name of the given font. This will use a java.awt.Font instance to create
	 * an lwjgl suitable TrueTypeFont.
	 * @param bold Whether or not the font will be bold.
	 * @param fontSize The size of the font.
	 * @param antiAlias Whether or not you want to use anti aliasing for the custom font.
	 */
	public void addCustomFont(String fontName, boolean bold, int fontSize, boolean antiAlias) {
		try {
			TrueTypeFont ttf = new TrueTypeFont(new Font(fontName, bold ? Font.BOLD : Font.PLAIN, fontSize), antiAlias);
			customFonts.put(fontName.toLowerCase(), ttf);
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println("Error adding custom font: " + fontName);
		}
	}

	/**
	 * Draws a String at the given x, y coordinates, using the specified customFont type.
	 * 
	 * @param string The string to be rendered.
	 * @param x The x coordinate in 2D space.
	 * @param y The y coordinate in 2D space.
	 * @param format The string format. 0 = align left, 1 = align right, 2 = align center
	 * @param customFont The name of the custom font. This would have been set when adding a
	 * new font using {@link #addCustomFont(String, boolean, int, boolean)}
	 */
	public void drawString(String string, int x, int y, int format, String customFont) {
		try {
			customFonts.get(customFont.toLowerCase()).drawString(x, y, string, 1, 1, format);
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println("Error drawing string using custom font: " + customFont);
		}
	}

	/**
	 * 
	 * Draws a String at the given x, y coordinates, using the specified customFont type.
	 * 
	 * @param string The string to be rendered.
	 * @param x The x coordinate in 2D space.
	 * @param y The y coordinate in 2D space.
	 * @param scaleX The scale (size) you want to use while drawing the string.
	 * @param scaleY The scale (size) you want to use while drawing the string.
	 * @param format The string format. 0 = align left, 1 = align right, 2 = align center
	 * @param customFont The name of the custom font. This would have been set when adding a
	 * new font using {@link #addCustomFont(String, boolean, int, boolean)}
	 */
	public void drawString(String string, int x, int y, int scaleX, int scaleY, int format, String customFont) {
		try {
			customFonts.get(customFont.toLowerCase()).drawString(x, y, string, scaleX, scaleY, format);
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println("Error drawing string using custom font: " + customFont);
		}
	}

	public static GLFont getInstance() {
		return INSTANCE;
	}

}

BufferedImage fontImage = getFontImage(ch);

Why do I see so many people doing this?

You only need to create that one image, use font metrics to get dimensions and render the character to the buffered image. If you just draw to one image, it decreases the load time per-font dramatically from 513+ images to one image created. While making 513+ buffered images is fine for one or two fonts, if you need to create a lot of fonts, then you are just adding unnecessary load time.

Even if you want font images to possibly be bigger than 512x512, the most images you should need is two (one for scratch and finding out how big the second one is, and the other for the font).

Because everyone copied Slick2D :P.

[quote]Because everyone copied Slick2D :P.
[/quote]
Well I wish people would realize the optimization to font loading that could be done :stuck_out_tongue:

Yeah, it’s hard for beginners. But there’s still a small amount of pride (and learning experience) compiling the snippets together into our own wrappers.

Well I wish people would realize the optimization to font loading that could be done :stuck_out_tongue:
[/quote]
Feel free to explain in depth :wink:

minigame:

So the way Slick2D loads fonts is by:

// load a backend buffered image for all the letters
// for (unicode characters 0-255) {
//         create an image for this letter that just calculates font metrics
//         create an image for this letter that draws with the font to it
//         draw the second image to the backend image
// }
// load the texture etc.

Resulting in 513 buffered images

But you can simply:

// load a backend buffered image for all the letters
// for (unicode characters 0-255) {
//         draw this letter at the correct position on the final image
//         adjust offsets per-character using backend's font metrics
// }
// load the texture etc

Resulting in 1 buffered image

Which should have a nice increase on font loading on anybody’s system.

Although if you really want a better speedup, then you can load the font once and then serialize character regions and RGBA data to feed directly to OpenGL and only create 1 buffered image ever per-font in a project.

i dont think that loading 3 to 5 fonts at startup will make a difference if it takes 800ms or 200ms.

Yer looking at all of that makes my eyes water , mine is an extremely crude but effective system that generates each letter from a bitmap onto a vbo.


public class TextGenerator {
	private String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789. ";
	private spritesheet alphabetsprite;
	private Core core;
	public TextGenerator(Core core){
		this.core = core;
		alphabetsprite = new spritesheet("textures/font1.png");
	}
	public Progressive_buffer[] generate_text(String text , Vertex2d position,int size){
	
		if(text == null || text.length() == 0 || position == null){
			return null;
		}
		Progressive_buffer[] drawdata = new Progressive_buffer[2];
		drawdata[0] = new Progressive_buffer(null,false);
		drawdata[1] = new Progressive_buffer(null,true);
		for(int i = 0; i < text.length();i++){
			char charat = text.charAt(i);
			int code = alphabet.indexOf(charat);
			if(code == -1){
				continue;
			}
			
			Progressive_buffer[] tempdata = core.G.rectangle((int)(position.x + (i * size)) ,(int)(position.y),size,(int)(size * 1.5), alphabetsprite.getcoords(code * 6, 0, (code+1)*6, 8));
			
			
			drawdata[0].extend(tempdata[0]);
			drawdata[1].extend(tempdata[1]);
	
		
		
		}
		
		
		return drawdata;
	}
	public Texture gettexture(){
		return alphabetsprite.gettexture();
	}
	public spritesheet getsprite(){
		return alphabetsprite;
	}
}

literally iterates over a string and then converts the product into a vbo. Don’t know much about its performance but I haven’t had any issues with it. It only works with fonts that have the same width though , bit of a downside could probably implement that.

@Icass

Would probably be better to just render the text to texture and then only render 1 texture instead of rendering VBO each time.

PS
dat code style though

Its not rendering each VBO at a time , its generating a pair of float buffers that can be bound or extended to a vbo.

PS
Yer the code is a bit messy