Rendering text with JOGL?

I am writing a JOGL application and I would like to display some text in my 3D environment. Two approaches I see are:

  • using an offscreen buffer, drawing to it using Java2D and then using the resulting image as a texture
  • writing the text as a vectored image, without going through a texture

Are there other approaches? And, does anyone know how to achieve the second approach using JOGL?

A third approach is modeling a font and rendering the characters.

For the second approach, take a look at this thread: http://www.java-gaming.org/forums/index.php?topic=11221.0

The way I’m doing it is via textures.
I use Java2D to render the text to a
custom Image and then use the
int[] pixels to create a texture. It has
been working very well and you can
do pre-processed effects at the
Java2D stage. The advantage of this
approach is speed and ability to use
shaders. The disadvantage is the
difficulty of dealing with power-of-2
sizes and images that are bigger than
the GPU’s limit (e.g. 1K, 2K, 4K).

[quote]The way I’m doing it is via textures.
I use Java2D to render the text to a
custom Image and then use the
int[] pixels to create a texture.
[/quote]
Thats all good and fancy, but how do you go about figuring out the size for the image you create? There a few factors I’ve thought about in regard to this:

-first, I want the image to be the smallest size possible to fit the text I want to write, so how do I go about figuring out what that would be?
-second, how do I go from this to dealing with the powers of 2 restrictions on the textures in OpenGL

Basically, to sum it up, how do you create the minimum possible dimension image that contains the desired text and yet still meets the powers of 2 restriction on the sizes?

Also, on something of a side note, how would I make the background of the image transparent so that the only thing that shows up is the text? I don’t want the rest of the image to write over anything I may already have displayed on the screen. I’m not that familiar with Java2D or the use of BufferedImages, so please bear with me. (naturally, pointing me to some sort of tutorial on how to perform this technique would be the best)

Thank you very much

Thats all good and fancy, but how do you go about figuring out the size for the image you create? There a few factors I’ve thought about in regard to this:

-first, I want the image to be the smallest size possible to fit the text I want to write, so how do I go about figuring out what that would be?
-second, how do I go from this to dealing with the powers of 2 restrictions on the textures in OpenGL

Basically, to sum it up, how do you create the minimum possible dimension image that contains the desired text and yet still meets the powers of 2 restriction on the sizes?
[/quote]
The basic way is: Check the pixel length of the string you will be drawing with FontMetrics -> stringWidth() and then chunk up to the next available power of 2. Like < 16 use 16, less then 32 use 32, etc. Also use FontMetrics to get the height and do the same thing.

When you create your BufferedImage, use: BufferedImage.TYPE_4BYTE_ABGR_PRE to get an alpha channel. The GL texture should use GL_RGBA - remember to byte flip the ABGR to RGBA or use the GL_ABGR_EXT extension if avaialble.

When you draw the text to the BufferedImage, clear the back alpha channel with an AlphaComposite.Clear, like:


    Graphics2D g2d = (Graphics2D) image.getGraphics();
    g2d.setComposite(AlphaComposite.Clear);
    g2d.fillRect(0,0,width, height);

Then, when you are ready to draw your text (or other calls), reset the AlphaComposite for SrcOver…like:


    g2d.setComposite(AlphaComposite.SrcOver);

Now upload your image.

Prior to rendering the quad, you must set the GL blend function. For alpha channel transparencies (like this), set the source to: GL_SRC_ALPHA and destination to: GL_ONE_MINUS_SRC_ALPHA

Like:


GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);

Render your quad and it should be see through. That’s about it. :slight_smile:

If you use a non-pre-multiplied BufferedImage,
then you don’t have to clear the alpha, and your
(Java2D) rendering will also be much faster.
Basically the idea is to let GPU do the alpha-blending
instead of the CPU since the GPU is much
faster for this.

.rex

Good point - PRE isn’t needed here.

I was showing how to clear to illustrate the use of AlphaComposite.Clear. The next question would have been: “How can I change the text without creating a new image? The new text is appearing on the old text” :slight_smile:

thanks a lot guys! thats some amazing advice and i really appreciate it. i imagine the nehe tutorials will be the best place to go for info on how to do texturing (though I have a rudimentary understanding, its not good enough). and yes… the next question would be how do i change the text :slight_smile:

actually a few more questions on some of the texture generating methods. The following code appears in the JOGL conversion of Nehe tutorial 6:


if (mipmapped)
      {
        glu.gluBuild2DMipmaps(target, GL.GL_RGB8, img.getWidth(), img.getHeight(), GL.GL_RGB, GL.GL_UNSIGNED_BYTE, dest);
      }
      else
      {
        gl.glTexImage2D(target, 0, GL.GL_RGB, img.getWidth(), img.getHeight(), 0, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, dest);
      }

would I replace GL.GL_RGB8 and GL.GL_RGB with GL.GL_ABGR_EXT in each place where they appear here (provided of course that I don’t want to go through the trouble of flipping the data around to RGBA)

Or if this doesn’t work and I have to flip the data to RGBA, would I then use GL.GL_RGBA8 and GL.GL_RGBA instead of their respective values in this code?

Yes, but it probably won’t work the the MipMap using the GL_ABGR_EXT extension. Just use the glTexImage2D to upload, mip mapped text can get ugly looking (to blocky to read on some cards).

How do I use a FontMetrics without an associated graphics object. JOGL components don’t use a Graphics Object. I tried the following:


		Graphics g = this.getGraphics();
		g.setFont(font);
		int str_width = g.getFontMetrics().stringWidth(text)+2;
		int str_height = g.getFontMetrics().getHeight()+2;

in the constructor of an object which inherits from GLJPanel, but when it runs the g object is null. How else can I pull this off (font and text are variables for my desired font and text, respectively)

Create an image object and get it’s graphics (you will need this anyway, because that’s what you eventually will upload).


   BufferedImage  img = new BufferedImage(iwidth, iheight, BufferedImage.TYPE_4BYTE_ABGR);
   Graphics2D g2d = (Graphics2D) img.createGraphics();

Then you can set it’s font, use the metrics, draw to it and eventually upload it to the video card.

Thanks

but now i get the following error


apple.awt.EventQueueExceptionHandler Caught Throwable : javax.media.opengl.GLException: glGetError() returned the following error codes after a call to glTexImage2D(): GL_INVALID_ENUM 
     [java] javax.media.opengl.GLException: glGetError() returned the following error codes after a call to glTexImage2D(): GL_INVALID_ENUM 
     [java]     at javax.media.opengl.DebugGL.checkGLGetError(DebugGL.java:9787)
     [java]     at javax.media.opengl.DebugGL.glTexImage2D(DebugGL.java:7043)
     [java]     at mvt.graphics.TextObject.makeRGBATexture(TextObject.java:188)
     [java]     at mvt.graphics.TextObject.glDraw(TextObject.java:117)
     [java]     at mvt.graphics.PlotPanel.display(PlotPanel.java:67)

from the following code


		// recall member variables:
		// - data: BufferedImage containing the image to draw
		// - texture: integer identifying the texture to OpenGL
		
		// grab the byte data from the BufferedImage and put it into the
		// 'dest' ByteBuffer
		byte [] info = ((DataBufferByte)data.getRaster().getDataBuffer()).getData();
		for (int i=0; i<info.length; i+=4) {
			// switch the ABGR data around to RBGA
			byte a = info[i];
			byte b = info[i+1];
			byte g = info[i+2];
			byte r = info[i+3];
			
			info[i] = r;
			info[i+1] = g;
			info[i+2] = b;
			info[i+3] = a;
		}
		dest = ByteBuffer.allocateDirect(info.length);
		dest.order(ByteOrder.nativeOrder());
		dest.put(info, 0, info.length);
		
		gl.glTexImage2D(
			texture,
			0,
			GL.GL_RGBA,
			data.getWidth(),
			data.getHeight(),
			0,
			GL.GL_RGBA,
			GL.GL_UNSIGNED_BYTE,
			dest
		);

i had tried it without flipping the bits, thinking the error might have been caused by using GL_ABGR_EXT as the image type, but that wasn’t the problem. Which of these enums is invalid?

That all looks ok. Two things to check:

  • Did you bind the texture (glGenTextures() and then glBindTexture())
  • Did you call this code from inside a glBegin/glEnd? (which would be bad)

err…on second thought, what is this:

gl.glTexImage2D(
texture,
0,
GL.GL_RGBA,
data.getWidth(),
data.getHeight(),
0,
GL.GL_RGBA,
GL.GL_UNSIGNED_BYTE,
dest
);

That should be GL_TEXTURE_2D

Before I come with my next question… thanks for all your help Vorax, i really appreciate it!! I’m not sure why the NEHE tutorial in JOGL for that had the texture variable there rather than that particular GLenum, but that advice worked.

I’ve got it all set up the way I think its supposed to be according to your instructions, yet all i get drawn is a black box. Here’s the code for the constructor (in which I create the buffered image)


   private String text;
    private Point2D.Double pt;
    private Plot2DPanel parent = null;
    private Color color;
    private Font font;
	private BufferedImage data;
	private int texture;
	private boolean textureMade;
    
    public TextObject(Point2D.Double pt, String text,
		      Plot2DPanel plotPanel, Color color, Font font){
			  
		super();
		this.text=text;
		this.pt = pt;
		this.parent = plotPanel;
		this.color = color;
		this.font = font;
		
		textureMade = false;
		
		BufferedImage temp = new BufferedImage(1,1, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics g = temp.getGraphics();
		g.setFont(font);
		int str_width = g.getFontMetrics().stringWidth(text)+2;
		int str_height = g.getFontMetrics().getHeight()+2;
		
		// create an image with dimensions that are powers of two
		// which is large enough to hold the desired text
		int width, height;
		for (width = 2; width<str_width; width *= 2)
			;
		for (height = 2; height<str_height; height *= 2)
			;
		
		// draw the string to a BufferedImage
		data = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics2D g2d = (Graphics2D) data.getGraphics();
		g2d.setColor(Color.white);
		g2d.setComposite(AlphaComposite.Clear);
		g2d.fillRect(0,0,width,height);
		
		g2d.setFont(font);
		g2d.setColor(color);
		g2d.setComposite(AlphaComposite.SrcOver);
		g2d.drawString(text,0,str_height);
    }

Here is the glDraw method (which gets called from the display() method of another class. this bit of the functionality works as i have it working in other classes)


public void glDraw(GL gl) {
		System.out.println("TextObject");
		
		// create the texture on the first time through only
		if (!textureMade) {
			System.out.println("Create Texture");
			// set up the texture we want to use
			gl.glEnable(GL.GL_TEXTURE_2D);
			texture = genTexture(gl);
			gl.glBindTexture(GL.GL_TEXTURE_2D, texture);
			makeRGBATexture(gl);
			gl.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MIN_FILTER,GL.GL_LINEAR);
			gl.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MAG_FILTER,GL.GL_LINEAR);
			textureMade = true;
		}
		
		double xmin = parent.getXMin();
		double xmax = parent.getXMax();
		double ymin = parent.getYMin();
		double ymax = parent.getYMax();
		
		double x = pt.getX();
		double y = pt.getY();
		
		// figure out how big the image is in our current GL coordinate system
		double quadWidth = (double)(data.getWidth()*(xmax-xmin)/parent.getWidth());
		double quadHeight = (double)(data.getHeight()*(ymax-ymin)/parent.getHeight());
		
		// using this information, we can position the quad and map the texture
		// containing the string to its 4 corners appropriately
		gl.glBindTexture(GL.GL_TEXTURE_2D, texture);
		
		gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2d(0.0, 0.0); gl.glVertex2d(x-quadWidth/2,y-quadHeight/2);
			gl.glTexCoord2d(1.0, 0.0); gl.glVertex2d(x+quadWidth/2,y-quadHeight/2);
			gl.glTexCoord2d(1.0, 1.0); gl.glVertex2d(x+quadWidth/2,y+quadHeight/2);
			gl.glTexCoord2d(0.0, 1.0); gl.glVertex2d(x-quadWidth/2,y+quadHeight/2);
		gl.glEnd();
		
	}

and, finally, this is the code that creates the texture (most of which was taken from the JOGL version of NEHE lesson 13)


	private int genTexture(GL gl) {
	
		final int[] tmp = new int[1];
		gl.glGenTextures(1, tmp, 0);
		return tmp[0];
	}
	
	private void makeRGBATexture(GL gl) {
	
		ByteBuffer dest = null;
		GLU glu = new GLU();
		
		// recall member variables:
		// - data: BufferedImage containing the image to draw
		// - texture: integer identifying the texture to OpenGL
		
		// grab the byte data from the BufferedImage and put it into the
		// 'dest' ByteBuffer
		byte [] info = ((DataBufferByte)data.getRaster().getDataBuffer()).getData();
		for (int i=0; i<info.length; i+=4) {
			// switch the ABGR data around to RBGA
			byte a = info[i];
			byte b = info[i+1];
			byte g = info[i+2];
			byte r = info[i+3];
			
			info[i] = r;
			info[i+1] = g;
			info[i+2] = b;
			info[i+3] = a;
		}
		dest = ByteBuffer.allocateDirect(info.length);
		dest.order(ByteOrder.nativeOrder());
		dest.put(info, 0, info.length);
		
		gl.glTexImage2D(
			GL.GL_TEXTURE_2D,
			0,
			GL.GL_RGBA,
			data.getWidth(),
			data.getHeight(),
			0,
			GL.GL_RGBA,
			GL.GL_UNSIGNED_BYTE,
			dest
		);
	}

I’m not sure why its giving me that black box. I’m sure its got something to do with the alpha value setting and then my manipulations of the bytes, since my knowledge about that is lacking. but then again i have never used textures before in anything, so there could always be a problem with the texture binding as well. thanks!

I left out this line:


gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);

but putting it in didn’t fix the problem. still just a black box for a texture

It’s a bit hard to tell without the full source, have you called:

glEnable(GL.GL_BLEND);

before you start rendering the quad? That might be why it’s black and not transparent

Also, this line looks suspicious:

g2d.drawString(text,0,str_height);

That appears to draw the text at the height of the string (which might be past the image boundary).

If neither of those work - I would start by painting the BufferedImage all in red or something (remove the AlphaComposite.clear first) to see that it’s on the quad at least red as expected.

Nope. I removed al alpha transparency and the like, made it a straight up 3byte RGB image, and its still a no go. I couldn’t see red text or anything even if it changed color.

Also, you have basically the entire code of the “TextObject” class right up there in my previous post, so everything that has anything to do with the texture is being called.

Also, I have the interesting side effect that anything else that is drawn on the screen when the texture object is drawn is drawn in black. I don’t have a clue why that could be happening, I never even call gl.glColor3f in this class.

Also, I have the interesting side effect that anything else that is drawn on the screen when the texture object is drawn is drawn in black.  I don't have a clue why that could be happening, I never even call gl.glColor3f in this class.

This is a clue - it’s something to do with your display() method or your init(). There is something messing with the state machine.