Making new technolgy do old things :D

posted in Not dead...
Published February 03, 2008
Advertisement
Bitmap fonts are about the most simple method of putting text on the screen; select a section of a texture, map it to a quad and bish-bash-bosh, done.

Of course, just because it works doesn't mean we can't play with it a bit now that hardware can do more [grin]

So, lets take a look at Bonsai's text printing routine;
int l_renderText(lua_State *L)	{		gfxEngineDetails * engine = CheckValidGraphicsObject(L);		Charset * cSet = (Charset*)luaL_checkudata(L,5,BONSAIFONT);		size_t len = 0;		const char * text = luaL_checklstring(L,4,&len);		float y = luaL_checknumber(L,3);		float x = luaL_checknumber(L,2);				if(lua_gettop(L) == 6)		{			setColorFromTable(L,-1);		}		else		{			glColor4f(1.0f, 1.0f, 1.0f, 1.0f);		}		if (engine->alphaTestEnabled)			glDisable(GL_ALPHA_TEST);				glEnable(GL_BLEND);		// Change blending mode		glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);		glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);		glEnable(GL_TEXTURE_2D);		for(size_t offset = 0; offset < len; offset++)		{			int CharX = cSet->Chars[text[offset]].x;			int CharY = cSet->Chars[text[offset]].y;			int Width = cSet->Chars[text[offset]].Width;			int Height = cSet->Chars[text[offset]].Height;			int OffsetX = cSet->Chars[text[offset]].XOffset;			int OffsetY = cSet->Chars[text[offset]].YOffset;			int Page = cSet->Chars[text[offset]].Page;			glBindTexture(GL_TEXTURE_2D,cSet->textures->at(Page));					glBegin(GL_QUADS);			glTexCoord2f(float(CharX) / float(cSet->Width), float(CharY) / float(cSet->Height) ); 			glVertex2f(x + float(OffsetX), y + float (OffsetY));			glTexCoord2f(float(CharX + Width) / float(cSet->Width), float(CharY) / float(cSet->Height) ); 			glVertex2f(x + float(OffsetX + Width), y + float (OffsetY));			glTexCoord2f(float(CharX + Width) / float(cSet->Width), float(CharY+Height) / float(cSet->Height) ); 			glVertex2f(x + float(OffsetX + Width), y + float (Height + OffsetY));			glTexCoord2f(float(CharX) / float(cSet->Width), float(CharY+Height) / float(cSet->Height) ); 			glVertex2f(x + float(OffsetX), y + float (Height + OffsetY));			glEnd();			x += float(cSet->Chars[text[offset]].XAdvance);		}		glDisable(GL_TEXTURE_2D);		glDisable(GL_BLEND);		if(engine->alphaTestEnabled)			glEnable(GL_ALPHA_TEST);		return 0;	}


Most of the openning pre-amble you can ignore, the intresting bit is the loop; here we grab each character in turn, get it's details, bind the texture the image is on and then render a quad with the texture mapped to it.

Simple, easy, but suffers from two 'issues';
- immediate mode
- texture binding

The latter isn't a huge issue as such, but with immediate mode going away in GL3 getting rid of it would be nice [smile] Also, that's a fair number of calls and because of the second issue we have to keep running begin..end pairs for each character; a performance nightmare.

The texture binding as I said is a problem; You can't bind a texture in a glBeing()..glEnd() block. Now, if everything is on the same 'page' then we can get rid of this, however if you want to be able to handle LARGE characters this can start to become an issue.

One way we could handle the restart problem is to do a check on which page we are on and what page we want, if they are different then called glEnd(), bind the new texture and then call glBegin() again.

So, we'll solve the latter problem first, because that allows us to solve the former at the same time.

How are we going to solve it? Enter 'texture arrays'.

Texture arrays are a DX10 technology which allow you to bind a number of textures to a single sampler and then select which one to sample from. In this instance we combine all the texture pages into a single texture array and use a fragment program to sample the correct texture page for the current character.

Thusly the new loop becomes;

glBindTexture(GL_TEXTURE_2D_ARRAY_EXT, cSet->textureArray);engine->textShader->use();engine->textShader->sendUniform("font",0);glBegin(GL_QUADS);for(size_t offset = 0; offset < len; offset++){	int CharX = cSet->Chars[text[offset]].x;	int CharY = cSet->Chars[text[offset]].y;	int Width = cSet->Chars[text[offset]].Width;	int Height = cSet->Chars[text[offset]].Height;	int OffsetX = cSet->Chars[text[offset]].XOffset;	int OffsetY = cSet->Chars[text[offset]].YOffset;	glTexCoord3f(float(CharX) / float(cSet->Width), float(CharY) / float(cSet->Height), page ); 	glVertex2f(x + float(OffsetX), y + float (OffsetY));	glTexCoord3f(float(CharX + Width) / float(cSet->Width), float(CharY) / float(cSet->Height), page  ); 	glVertex2f(x + float(OffsetX + Width), y + float (OffsetY));	glTexCoord3f(float(CharX + Width) / float(cSet->Width), float(CharY+Height) / float(cSet->Height), page  ); 	glVertex2f(x + float(OffsetX + Width), y + float (Height + OffsetY));	glTexCoord3f(float(CharX) / float(cSet->Width), float(CharY+Height) / float(cSet->Height), page  ); 	glVertex2f(x + float(OffsetX), y + float (Height + OffsetY));		x += float(cSet->Chars[text[offset]].XAdvance);}glEnd();


Note how the glBegin()/glEnd() pair have been moved outside the loop, yep, no more rebinding. Instead the 3rd texture coordinate is used to sample the correct page, as per the following fragment program;

// Fragment program to render text from a texture array#version 110#extension GL_EXT_texture_array : enableuniform sampler2DArray font;void main(void){	gl_FragColor = texture2DArray(font, gl_TexCoord[0].stp) * vec4(1.0, 0.0, 0.0, 1.0);}


texture2DArray is what takes care of the sampling, with the 'p' coordinate dealing with which page to sample from.

Now, this is all very well and nice but we are still having to make 8 immediate mode calls (and 80 bytes of data) per character and I couldn't help but think there is a better way to handle this; welcome to geometry shader abuse [grin]

The geometry shader is a nice extra level of control where you can make extra vertices and thus primatives before you hit the clipping hardware and fragment shader. While you can do all manner of things with it what we are intrested in here is a variation on a point sprite.

The idea is that instead of submitting all that data we only submit what is required and build the quad in the geometry shader instead.

Looking at the above code we can extract the following information;
- all texture coords are divided by the width and height of the textures which are constants; so these will be uniform values
- the positions are just offsets from a given (x,y) position by the height and width of the character; so we can pass that location as the location and just supply the per character width and height
- Finally, we need the character position in the font sheet, so that we can build up the texture coordinates, and which page it is on.

This clocks in at a huge 28bytes, which leaves 4 bytes until the min transfer window so you could even include a Z-coord in order to give layered rendering.

You may have noticed that I'm supplying per character data still but taking up much less space, that's because as you'll see in the code in a moment we are only send the data once for each character via a point. As the point doesn't get interpolated the data is effectively constant for the primative post-vertex shader.

So, our main loop now looks like this;
glBindTexture(GL_TEXTURE_2D_ARRAY_EXT, cSet->textureArray);// Bind GLSL to do the reading hereengine->textShader->use();engine->textShader->sendUniform("font",0);engine->textShader->sendUniform("texWidth",float(cSet->Width));engine->textShader->sendUniform("texHeight",float(cSet->Height));int dimention = engine->textShader->getAttributeLocation("charDimentions");int position = engine->textShader->getAttributeLocation("cPos");glBegin(GL_POINTS);for(size_t offset = 0; offset < len; offset++){	int CharX = cSet->Chars[text[offset]].x;	int CharY = cSet->Chars[text[offset]].y;	int Width = cSet->Chars[text[offset]].Width;	int Height = cSet->Chars[text[offset]].Height;	int OffsetX = cSet->Chars[text[offset]].XOffset;	int OffsetY = cSet->Chars[text[offset]].YOffset;	float page = float(cSet->Chars[text[offset]].Page);	glVertexAttrib3f(position, float(CharX), float(CharY), page);	glVertexAttrib2f(dimention, float(Width), float(Height));	glVertex2f(x + float(OffsetX), y + float(OffsetY));	x += float(cSet->Chars[text[offset]].XAdvance);}glEnd();


The vertex shader is pretty simple;
#version 110attribute vec3 cPos;attribute vec2 charDimentions;varying vec3 characterPosition;varying vec2 dimentions;void main(void){	characterPosition = cPos;	dimentions = charDimentions;	gl_FrontColor = gl_Color;	gl_Position = gl_Vertex;}	


The main thing to note here is the gl_Vertex pass though; normally in the vertex shader you transform the incoming vertices, however as all our later calculations are based on being rendering directly to the screen via an Ortho project changing to 'clip space' via a normal transform just makes life harder for us at this point.

The fragment shader is just as before'
// Fragment program to render text from a texture array#version 110#extension GL_EXT_texture_array : enableuniform sampler2DArray font;void main(void){	gl_FragColor = texture2DArray(font, gl_TexCoord[0].stp) * vec4(1.0, 0.0, 0.0, 1.0);}


The real 'fun' is the geometry shader, this is what does the grunt work for us and produces the output.

However, before you can use a geometry shader you have to tell OpenGL how many vertices (at most) you'll be emiting, the geometry data you'll be taking in and what you'll be throwing out.

In this case I do all this at window creation time;
engine->textShader = new GLSLProgram("text.vp","text.fp","text.gp");engine->textShader->setProgramParameters(GL_GEOMETRY_VERTICES_OUT_EXT, 4);engine->textShader->setProgramParameters(GL_GEOMETRY_OUTPUT_TYPE_EXT, GL_TRIANGLE_STRIP);engine->textShader->setProgramParameters(GL_GEOMETRY_INPUT_TYPE_EXT, GL_POINTS);engine->textShader->link();


This snippet basically loads in the shaders via some code I've got for handling GLSL stuff (upgraded a bit to deal with geometry shaders), then informs GL that we will only throw out 4 vertices per geometry shader instance run, that we will be giving back triangle strips and that we expect points when being used.

All this has to happen before the link stage, so that is how we end.

So, the geometry shader itself; It is infact pretty simple as it's a direct translation of the C++ code to GLSL for the most part;
// Geometery shader to perform text rendering#version 120#extension GL_EXT_geometry_shader4 : enable uniform float texWidth;uniform float texHeight;varying in vec2 dimentions[];varying in vec3 characterPosition[];void main(void){	// x, y, page	gl_TexCoord[0] = vec4(characterPosition[0].x / texWidth, characterPosition[0].y / texHeight, characterPosition[0].z, 1.0);	gl_Position = gl_ModelViewProjectionMatrix * gl_PositionIn[0];	EmitVertex(); 		gl_TexCoord[0] = vec4((characterPosition[0].x + dimentions[0].x) / texWidth, characterPosition[0].y / texHeight, characterPosition[0].z, 1.0);	gl_Position = gl_ModelViewProjectionMatrix * (gl_PositionIn[0] + vec4(dimentions[0].x, vec3(0.0)));	EmitVertex(); 		gl_TexCoord[0] = vec4((characterPosition[0].x) / texWidth, (characterPosition[0].y + dimentions[0].y)/ texHeight, characterPosition[0].z, 1.0);	gl_Position = gl_ModelViewProjectionMatrix * (gl_PositionIn[0] + vec4(0.0, dimentions[0].y, vec2(0.0)));	EmitVertex();		gl_TexCoord[0] = vec4((characterPosition[0].x + dimentions[0].x) / texWidth, (characterPosition[0].y + dimentions[0].y)/ texHeight, characterPosition[0].z, 1.0);	gl_Position = gl_ModelViewProjectionMatrix * (gl_PositionIn[0] + vec4(dimentions[0].x, dimentions[0].y, vec2(0.0)));	EmitVertex(); 		EndPrimitive();}


Firstly, you have to have "#version 120" at the top in order to use the functions provided by the EXT_gpu_shader4 extension which the geometry shader extension hooks into. You also need to inform the compiler that we'll be using said extension, this should error out if we try to use it on hardware which doesn't support it.

After that we have the two uniforms as mentioned before and 2 input varyings which come from the vertex shader.

The main body is pretty simple;
- We calculate the texture coordinates in the same way we did in C++
- Next we calculate the position of the vertex by adding the width and height as required and then finally transforming it into clipspace via the ModelViewProjectionMatrix.

This is repeated 4 times for our 'quad', each time calling 'EmitVertex' in order to tell the hardware we are done and finally finishing with a call to EndPrimaitive() which finishes up the processing for this primitive.

Note, things aren't quit in the same order as they were in the C++ code, vertex wise; that's because we submitted that as a QUAD type, however the geometry shader outputs triangle strips so we have to take that into account when we render.

So instead of going;
- Top Left
- Top Right
- Bottom Right
- Bottom Left
We pass it as
- Top Left
- Top Right
- Bottom Left
- Bottom Right
With the first 3 and the last 3 forming the two triangles.

So there you have it; texture rendering via texture arrays and geometry shaders.

At this point it's a trival matter to convert from immediate mode to VBO rendering as it's just a matter of putting the same data in the VBO.

Now, you might be thinking 'thats cool and all that, but text rendering is hardly a bottle neck' to which I would reply, 'true.. but as you said, firstly it's cool and secondly using this technique we could rendering ANYTHING which can be expressed as a quad and have it's texture placed into an array; such all the sprites in a game'.

All the code is in the Bonsai SVN as part of the head; technically it's not finished as I need to come up with the code for patching the Lua function table at runtime/window creation time to replace normal rendering with this system if the hardware supports it. Oh, and currently afaik this will only work on NV DX10 hardware (GF8 series stuffs) as ATI/AMD currently don't have these extensions supported.

Oh, and related to 'cool extensions'; GL_EXTX_framebuffer_mixed_formats
I wonder what that'll allow us to do? mixed formats bound to an FBO? ooo that would be shiney indeed [grin]
0 likes 1 comments

Comments

MARS_999
Nice, ATI still doesn't have texture arrays, WTF, that extension is one of the best in quite sometime. It's a must for my game, as I am sick of the texture atlas method and being only limited to 16 textures per pass. No reason why this shouldn't be out on ATI, all they have to do is, convert the 3D Texture to stop filtering between layers...
February 07, 2008 03:48 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Advertisement