Jump to content


OpenGL/GDI Font


31 replies to this topic

#1 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 21 December 2006 - 03:00 PM

Here is an OpenGL/GDI Font, inspired by D3DFont. Its pretty straight forward to use.
Just create the font object specifying the name of the typeface and the rest of the attributes and then call initialise.
When you're rendering you scene call render with your string and the coordinates you require.
As usual, I'm aiming for simplicity, and tried to keep dependencies down to a minimum.
I only really use this as a debugging tool so its not the most efficient possible. However its not bad, with very few tweaks you can use this in a more efficient way.
I havent bothered to do any checking for large font sizes. It will probably crash.

You could export the texture file (look at where the texture is uploaded at gluBuild2DMipmaps) and use that on non-windows platforms. Of course, you'd have to use public domain typeface.

Let me know what you think!

Here is the code which you can hopefully just drag and drop into your Windows/OpenGL project.

Font.h
#ifndef INCLUDE_FONT_H
#define INCLUDE_FONT_H

#include <Windows.h>
#include <string>

class Font
{
public:

	Font(const char* font, unsigned int size, bool bold, bool italic);
	~Font();

	bool initialise();

	void render(const char* pstr, float x, float y);
	void getTextSize(const char* pstr, float &xOut, float &yOut) const;

private:
	float renderChar (int ch, float x, float y) const;

	unsigned int width_;
	unsigned int height_;
	unsigned int spacing_;
	unsigned int texture_;
	unsigned int fontSize_;

	std::string name_;
	float   texCoords_[128-32][4];

	bool bold_;
	bool italic_;

	enum PaintResult {
		Fail,
		MoreData,
		Success,

	};

	bool createGDIFont( HDC hDC, HFONT* pFont );
	PaintResult paintAlphabet( HDC hDC, bool bMeasureOnly );
};
#endif

Font.cpp
#include "Font.h"

#include <math.h>
#include <gl/GLU.h>
#include <fstream>

Font::Font(const char* font, unsigned int size, bool bold, bool italic) : 
	bold_(bold), italic_(italic), name_(font), texture_(0), width_(128),
	height_(128), fontSize_(size), spacing_(fontSize_ / 3)
{

}

Font::~Font()
{
	if (texture_)
	{
		glDeleteTextures(1, &texture_);
	}
}

bool Font::initialise()
{
	HDC hDC = CreateCompatibleDC( NULL );
	SetMapMode( hDC, MM_TEXT );

	HFONT hFont = NULL;
	bool ok = createGDIFont( hDC, &hFont );

	if (ok)
	{
		HFONT hFontOld = (HFONT) SelectObject( hDC, hFont );

		PaintResult p;
		while(  MoreData == (p = paintAlphabet( hDC, true )) )
		{
			width_ *= 2;
			height_ *= 2;
		}
		ok = p == Success;
		if (ok)
		{
			// Prepare to create a bitmap
			DWORD*      pBitmapBits;
			BITMAPINFO bmi;
			ZeroMemory( &bmi.bmiHeader, sizeof(BITMAPINFOHEADER) );
			bmi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
			bmi.bmiHeader.biWidth       =  (int)width_;
			bmi.bmiHeader.biHeight      = -(int)height_;
			bmi.bmiHeader.biPlanes      = 1;
			bmi.bmiHeader.biCompression = BI_RGB;
			bmi.bmiHeader.biBitCount    = 32;

			// Create a bitmap for the font
			HBITMAP hbmBitmap = CreateDIBSection( hDC, &bmi, DIB_RGB_COLORS,
				(void**)&pBitmapBits, NULL, 0 );

			HGDIOBJ hbmOld = SelectObject( hDC, hbmBitmap );

			// Set text properties
			SetTextColor( hDC, RGB(255,255,255) );
			SetBkColor(   hDC, RGB(0,0,0) );
			SetTextAlign( hDC, TA_TOP );

			// Paint the alphabet onto the selected bitmap
			ok = paintAlphabet( hDC, false ) == Success;

			if (ok)
			{
				glGenTextures(1, &texture_);
				BITMAP	bitmap;
				GetObject(hbmBitmap,sizeof(BITMAP), &bitmap);
				glBindTexture(GL_TEXTURE_2D, texture_);	// Bind Our Texture
					glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);

				// Its actually BGRA, but since we're rendering white...
				gluBuild2DMipmaps(GL_TEXTURE_2D, 4, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, bitmap.bmBits);

			}

			SelectObject( hDC, hbmOld );
			SelectObject( hDC, hFontOld );
			DeleteObject( hbmBitmap );
			DeleteObject( hFont );
			DeleteDC( hDC );
		}
	}

	return ok;
}

bool Font::createGDIFont( HDC hDC, HFONT* pFont )
{
	int nHeight    = fontSize_;
	DWORD dwBold   = bold_   ? FW_BOLD : FW_NORMAL;
	DWORD dwItalic = italic_ ? TRUE    : FALSE;
	*pFont         = CreateFont( nHeight, 0, 0, 0, dwBold, dwItalic,
		FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
		CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
		VARIABLE_PITCH, name_.c_str() );

	return ( *pFont != NULL );
}

Font::PaintResult Font::paintAlphabet( HDC hDC, bool bMeasureOnly )
{
	SIZE size;
	char str[2] = "x";

	// Calculate the spacing between characters based on line height
	if( 0 == GetTextExtentPoint32( hDC, str, 1, &size ) )
		return Fail;
	spacing_ = (int)ceil(size.cy * 0.3f);

	// Set the starting point for the drawing
	DWORD x = spacing_;
	DWORD y = 0;

	// For each character, draw text on the DC and advance the current position
	for( char c = 32; c < 127; c++ )
	{
		str[0] = c;
		if( 0 == GetTextExtentPoint32( hDC, str, 1, &size ) )
			return Fail;

		if( (DWORD)(x + size.cx + spacing_) > width_ )
		{
			x  = spacing_;
			y += size.cy + 1;
		}

		// Check to see if there's room to write the character here
		if( y + size.cy > height_ )
			return MoreData;

		if( !bMeasureOnly )
		{
			// Perform the actual drawing
			if( 0 == ExtTextOut( hDC, x+0, y+0, ETO_OPAQUE, NULL, str, 1, NULL ) )
				return Fail;

			texCoords_[c-32][0] = ((FLOAT)(x))/width_;
			texCoords_[c-32][1] = ((FLOAT)(y))/height_;
			texCoords_[c-32][2] = ((FLOAT)(x + size.cx))/width_;
			texCoords_[c-32][3] = ((FLOAT)(y + size.cy))/height_;
		}
		x += size.cx + (2 * spacing_);
	}

	return Success;
}

float Font::renderChar (int ch, float x, float y) const
{
	float tx1 = texCoords_[ch-32][0];
	float ty1 = texCoords_[ch-32][1];
	float tx2 = texCoords_[ch-32][2];
	float ty2 = texCoords_[ch-32][3];

	float w = (tx2-tx1) * width_ ;
	float h = (ty2-ty1) * height_;

	glTexCoord2f(tx1, ty2);
	glVertex2f(x, y);

	glTexCoord2f(tx2, ty2);
	glVertex2f(x+w, y);

	glTexCoord2f(tx2, ty1);
	glVertex2f(x+w, y+h);

	glTexCoord2f(tx1, ty1);
	glVertex2f(x, y+h);

	return w;
}

void Font::render(const char* pstr, float xs, float ys)
{
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_COLOR, GL_ONE);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texture_);
	glBegin (GL_QUADS);
	float x = xs;
	float y = ys - fontSize_;
	for (const char *ptr = pstr; *ptr; ++ptr)
	{
		if (*ptr == ' ')
		{
			x += spacing_;
		}
		else if (*ptr == '\n')
		{
			y -= fontSize_;
			x = xs;
		}	
		else if( (*ptr-32) < 0 || (*ptr-32) >= 128-32 )
		{
		}
		else
		{
			x += renderChar(*ptr, x, y);		
		}
	}	
	glEnd();
	glDisable(GL_TEXTURE_2D);

	glDisable(GL_BLEND);
}


void Font::getTextSize(const char* pstr, float &xOut, float &yOut) const
{
	float x = 0;
	float y = 0;
	for (const char *ptr = pstr; *ptr; ++ptr
	{
		if (*ptr == ' ')
		{
			x += spacing_;
		}
		else if (*ptr == '\n')
		{
			y += fontSize_;
			xOut = std::max<float>(xOut, x);
		}	
		else if( (*ptr-32) < 0 || (*ptr-32) >= 128-32 )
		{
		}
		else
		{
			float tx1 = texCoords_[*ptr-32][0];
			float tx2 = texCoords_[*ptr-32][2];
			x += (tx2-tx1) * width_ ;		
		}
	}	
	yOut = y;

}


#2 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 12 February 2007 - 01:28 PM

How about a simple demo using it?

#3 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 12 February 2007 - 10:29 PM

Its pretty simple, but just for you, I've adapted a glut example


// Dave Hillier, adapted from GLUT example

/* Copyright (c) Mark J. Kilgard, 1994. */

/* This program is freely distributable without licensing fees 
and is provided without guarantee or warrantee expressed or 
implied. This program is -not- in the public domain. */

#include <string.h>
#include <GL/glut.h>

#include "Font.h"

Font *font = NULL;
Font *fonts[3];

char defaultMessage[] = "GLUT means OpenGL.";
char *message = defaultMessage;

void
selectFont(int newfont)
{
	font = fonts[newfont];
	glutPostRedisplay();
}

void
selectMessage(int msg)
{
	switch (msg) {
  case 1:
	  message = "abcdefghijklmnop";
	  break;
  case 2:
	  message = "ABCDEFGHIJKLMNOP";
	  break;
	}
}

void
selectColor(int color)
{
	switch (color) {
  case 1:
	  glColor3f(0.0, 1.0, 0.0);
	  break;
  case 2:
	  glColor3f(1.0, 0.0, 0.0);
	  break;
  case 3:
	  glColor3f(1.0, 1.0, 1.0);
	  break;
	}
	glutPostRedisplay();
}

void
tick(void)
{
	glutPostRedisplay();
}

void
output(int x, int y, char *string)
{

	font->render(string, x, 150-y);
}

void
display(void)
{
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
	output(0, 0, "This is written in a GLUT bitmap font.");
	output(100, 65, message);
	output(50, 120, "(positioned in pixels blah blah)");
	glutSwapBuffers();
}

void
reshape(int w, int h)
{
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluOrtho2D(0, w, 0, h);
	glMatrixMode(GL_MODELVIEW);
}

int
main(int argc, char **argv)
{
	int msg_submenu, color_submenu;

	glutInit(&argc, argv);


	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(500, 150);
	glutCreateWindow("Font.cpp bitmap font example");
	glClearColor(0.0, 0.0, 0.0, 1.0);

	fonts[0] = new Font("Arial", 15, false, false);
	fonts[1] = new Font("Times New Roman", 10, false, false);
	fonts[2] = new Font("Verdana", 24, false, false);
	for (int i = 0; i < 3; ++i)
		fonts[i]->initialise();

	font = fonts[2];


	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutIdleFunc(tick);
	msg_submenu = glutCreateMenu(selectMessage);
	glutAddMenuEntry("abc", 1);
	glutAddMenuEntry("ABC", 2);
	color_submenu = glutCreateMenu(selectColor);
	glutAddMenuEntry("Green", 1);
	glutAddMenuEntry("Red", 2);
	glutAddMenuEntry("White", 3);
	glutCreateMenu(selectFont);
	glutAddMenuEntry("Arial 15", 0);
	glutAddMenuEntry("Times New Roman 10", 1);
	glutAddMenuEntry("Verdana 24", 2);
	glutAddSubMenu("Messages", msg_submenu);
	glutAddSubMenu("Color", color_submenu);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
	glutMainLoop();

	for (int i = 0; i < 3; ++i)
		delete fonts[i];

	return 0;             /* ANSI C requires main to return int. */
}


#4 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 13 February 2007 - 07:48 AM

Do you know what's going on?

Posted Image

I just changed the background color of font and display to see outline of characters better.
Didn't change anything else.

#5 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 13 February 2007 - 08:22 AM

It should look like this:
Posted Image

If I change the background colour, like this:
Posted Image

#6 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 13 February 2007 - 03:42 PM

Ah, I think I see the problem. The posted code is bad.

You posted:
else if (*ptr == 'n')

and it should be:
else if (*ptr == '\n')


(*ptr == '\n')


Code tags work ok for me. Is there a reason why the code was posted bad?
Any other errors I miss?

#7 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 13 February 2007 - 05:23 PM

Very odd... I've not made any changes to the code since posting it.
Perhaps it was the way I cut and paste it.... maybe.
There are two instances of 'n' that need changing to '\n' (one in getTextSize, the other in render)

#8 Reedbeta

    DevMaster Staff

  • Administrators
  • 5340 posts
  • LocationSanta Clara, CA

Posted 13 February 2007 - 05:41 PM

Hmm...I made the changes. I think this might have to do with the string not being properly MySQL escaped when submitted through the COTD form. I always have to add backslashes on quotes and things when I use it.
reedbeta.com - developer blog, OpenGL demos, and other projects

#9 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 13 February 2007 - 09:36 PM

What I would find most useful in a OpenGL Font renderer, would be a function that
would mimic the parameters of TextOut(). Basically, an OpenGL version of TextOut().
Both functions should render text in the exactly the same location on screen, at
exactly the same scale and size.

#10 Reedbeta

    DevMaster Staff

  • Administrators
  • 5340 posts
  • LocationSanta Clara, CA

Posted 13 February 2007 - 10:29 PM

Lost, what does TextOut do that this sample does not? Both of them just take a string and a location at which to display it. The only other thing I can see that TextOut does is responds to left/center/right alignment settings, but that could easily be added to this class.

EDIT: another interesting extention I can think of is to let the render function take printf-style varargs...this would be pretty easy to glom on with vsnprintf :)
reedbeta.com - developer blog, OpenGL demos, and other projects

#11 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 14 February 2007 - 01:42 AM

TexOut() takes logical coordinates, while the above code takes pixel coordinates,
so they don't render text at the same location on the screen.

The exact location, size and scale is important for me, because I might want
to print it out. In other words, you could use OpenGL's TextOut() as a preview,
and then use GDI's TextOut() for printing.

If I could, I would use TextOut() directly on the backbuffer, but you can't.
http://support.microsoft.com/kb/131024

#12 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 14 February 2007 - 03:23 AM

I rendered GDI's TextOut() on the top, and font.cpp on the bottom, same font style and font size,
spacing seems to be off by some pixels:

Posted Image

I am serious about trying to create an exact copy of GDI TextOut() down to the last pixel. :)

I'm not sure if this has to do with OpenGL's pixel rasterization?
Microsoft has some recommendations.

The rasterization rules are specific:

Quote

To obtain exact two-dimensional rasterization, carefully specify both the orthographic projection and the vertices of the primitives that are to be rasterized. Specify the orthographic projection with integer coordinates, as shown in the following example:
gluOrtho2D(0, width, 0, height);
The parameters width and height are the dimensions of the viewport. Given this projection matrix, place polygon vertices and pixel image positions at integer coordinates to rasterize predictably. For example, glRecti(0, 0, 1, 1) reliably fills the lower-left pixel of the viewport, and glRasterPos2i(0, 0) reliably positions an unzoomed image at the lower-left pixel of the viewport. However, point vertices, line vertices, and bitmap positions should be placed at half-integer locations. For example, a line drawn from (x (1) , 0.5) to (x (2) , 0.5) will be reliably rendered along the bottom row of pixels in the viewport, and a point drawn at (0.5, 0.5) will reliably fill the same pixel as glRecti(0, 0, 1, 1).

An optimum compromise that allows all primitives to be specified at integer positions, while still ensuring predictable rasterization, is to translate x and y by 0.375, as shown in the following code sample. Such a translation keeps polygon and pixel image edges safely away from the centers of pixels, while moving line vertices close enough to the pixel centers.

glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity( );
gluOrtho2D(0, width, 0, height);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity( );
glTranslatef(0.375, 0.375, 0.0);
/* render all primitives at integer positions */


#13 Reedbeta

    DevMaster Staff

  • Administrators
  • 5340 posts
  • LocationSanta Clara, CA

Posted 14 February 2007 - 05:28 AM

Well, I admit I'm "lost" as to why you would care so much about a few single-pixel errors - I don't see why you can't just use TextOut itself. But, if you want, feel free to take dave's code and modify it to fix the offsets.
reedbeta.com - developer blog, OpenGL demos, and other projects

#14 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 14 February 2007 - 08:42 AM

If you want an exact copy of what you get in text out, you should probably render the entire string to texture then display that instead of the technique I'm using. But in the case you've shown, it looks like the space is just 1 pixel to large. Perhaps spacing_ should be (fontSize_ / 3) - 1

The font probably renders the same as D3DFont.

#15 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 14 February 2007 - 09:51 AM

Reedbeta said:

Well, I admit I'm "lost" as to why you would care so much about a few single-pixel errors
A few pixel errors will probably turn into many pixel errors when the font size is enlarged.

Reedbeta said:

I don't see why you can't just use TextOut itself.
Because Microsoft won't let me do it. I posted a technical link above.

dave_ said:

If you want an exact copy of what you get in text out, you should probably render the entire string to texture then display that instead of the technique I'm using. But in the case you've shown, it looks like the space is just 1 pixel to large. Perhaps spacing_ should be (fontSize_ / 3) - 1
Subtracting 1 worked for Arial and Verdana, but not for Times New Roman,
so spacing seems to be complicated. And rendering out an entire line is not
practical for me, as length will always be an issue. Per character is best.

#16 Reedbeta

    DevMaster Staff

  • Administrators
  • 5340 posts
  • LocationSanta Clara, CA

Posted 14 February 2007 - 03:43 PM

Lost said:

Because Microsoft won't let me do it. I posted a technical link above.

Right, but why would you want to use TextOut on an OpenGL backbuffer anyway? You mentioned using it to construct some screen preview for printing, but if that's the case all your printing has to be done using GDI anyway, so your screen preview can be done in GDI too, and you could just use TextOut itself. :)

And I don't get your point about multi-pixel errors when the text is enlarged. Proportional to the size of the text the errors will still be very, very small. Really, why bother worrying about such small offsets? It's not like TextOut is some paragon of typographical perfection anyway.
reedbeta.com - developer blog, OpenGL demos, and other projects

#17 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 14 February 2007 - 07:08 PM

I want to use elements of OpenGL and TextOut() together (interactively), and the
only way to do that is to render TextOut() to the backbuffer. But this is prohibited.
The exact position, size and scale of the font is important. The OpenGL version
of TextOut() must match its GDI equivalent exactly.

#18 Reedbeta

    DevMaster Staff

  • Administrators
  • 5340 posts
  • LocationSanta Clara, CA

Posted 14 February 2007 - 09:09 PM

Lost said:

The exact position, size and scale of the font is important. The OpenGL version
of TextOut() must match its GDI equivalent exactly.

You still haven't answered my question: why is it so important? Honestly, I can't think of a good reason.
reedbeta.com - developer blog, OpenGL demos, and other projects

#19 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 14 February 2007 - 11:14 PM

Lost said:

only way to do that is to render TextOut() to the backbuffer.
It's not the only way, you can render to texture.

#20 Lost

    New Member

  • Members
  • PipPip
  • 27 posts

Posted 15 February 2007 - 07:18 AM

Quote

You still haven't answered my question: why is it so important? Honestly, I can't think of a good reason.
Needless to say, I need it, so let's just agree to that. :)
WYSIWYG. It doesn't make much sense to print something out
that doesn't look exactly what you created. I hope you understand that.

Quote

It's not the only way, you can render to texture.

I could probably create a pbuffer the same size as the backbuffer, and copy it
over at the end of every render. It just depends on how much video memory is
available, as all buffers must be created in video memory.

I might have a screen display of 1024x768x32 or larger, and might also want to
create some extra pbuffers for other work in addition to the 3 buffers already
being used for flipping (font/back/pbuffer copy).

I'm guessing most 3D cards nowadays come with 128MB or more?





1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users