DEATHSHADOW'S
MADNESS
Madness and You, Perfect together!

  1. Top of Page
  2. Main Menu
  3. Page Content
  4. Page Extras
  5. Bottom of Page

glKernedFont - The Code

Drawing single characters is all well and good, but some other functions to make life simpler are in order. Lets go over the unit section by section.


{$COPERATORS ON}
unit glKernedFont;

I've been using other languages for a bit too long, and the C style shortcuts of += and -= are just too convenient not to have.

Interface


uses
  gl,
  glu;

Of course we need openGL, and we need the GLU unit to auto-generate mip-maps of the raster font, which is why it doesn't look as bad when you resize it.


type
  tglKernedChar=array[0..5] of shortInt;
  tglKernedTable=array[32..127] of tglKernedChar;
  tglKernedHeader=record
    formatRevision,
    bufferWidth,bufferHeight,
    charWidth,charHeight,
    dataStart:dword;
    kerningTable:tglKernedTable;	
    reserved:array[0..127] of byte;
  end;

The first two types are used to store our kerning info, the other is obviously our header structure.


  tglKernedFont=object
    name:string;

    texture:gluInt;

    info:tglKernedHeader;

    renderX,renderY,renderZ,
    charGlHeight,charSpacing,
    charAddWidth,charAddHeight
    charScale,charScaleWidth,charScaleHeight:glFloat;

    sourceWidth,sourceHeight:longint;

    lastKern:tglKernedChar;

I store the name of the font just in case you lose track and want to see what one you are loading/calling. The texture variable pretty self exlpanatory. The info variable is what we read our file header into, renderX, renderY and renderZ are the print cursor coordinates, used to handle the auto kerning without going nutzo on glTransform and push/popmatrix chicanery. charAddWidth and charAddHeight are adjusted into texture coordinates. charGLHeight is the ratio of pixels in the font to your viewpoint metrics. charScale, scaleWidth and scaleHeight are used by our scale function, for when you don't want to use glScale. sourceWidth and sourceHeight are the actual height and width of our pixel buffer, lastKern is the kerning info from the last character, and of course kernTable is the actual kerning info for each character.

Next up are our functions:


    constructor init(fontName:string; glHeight:glFloat);
    procedure scale(s:glFloat);
    procedure setposition(x,y,z:glFloat);
    function stringSize(st:string):glFloat;
    procedure writeChar(ch:char);
    procedure writeString(st:string);
    procedure writeStringCentered(st:string);
    procedure writeStringRight(st:string);
    destructor term;
  end;

The constructor is pretty no nonsense, as are most of the rest of our functions. In addition to writing out just the one character, I've added string functions that will render a string 'normal', centered off the current position, and justified right. To pull off the latter two we need to know how wide the string is going to be, and since variable widths are fluid and can overlap, the only reliable way is to have a 'stringSize' function to pull that width without actually rendering anything.

Implementation

First up we have our kerning comparison function. We just send this the previous letter's kerning, this letter's kerning, and it returns whatever sum of the two meeting edges gives the widest value.


function kernCompare(prev,this:tglKernedChar):glFloat;
var
  t:word;
  n,test:glFloat;
begin
  n:=-999;
  for t:=0 to 2 do begin
    test:=prev[t+3]+this[t];
    if test>n then n:=test;
  end;
  kernCompare:=n;
end;

Rather than get too fancy on the logic, I just set my result to an absurd negative value, then add the two values together, if it's greater than the result, use the new value as the result.

Next we have our constructor. Let's break this into sections since it's rather lengthy. First our declaration and variables, and setting one value.


constructor tglKernedFont.init(fontName:string; glHeight:glFloat);
var
  f:file;
  fBuffer,fPoint,fBufferEnd,
  pBuffer:^byte;
  pPoint:^word;
  t,b:word;
  fBufferSize,pBufferSize:dword;
begin
  name:=fontName;

File handler, and our various pointers. Only fBuffer and pBuffer will be 'fixed' pointers to the buffer. fBufferEnd is the end of our file buffer which we'll use to do our loop without a counter. fPoint and pPoint are our current position in the buffers. b is storage for the color being written in our encoded stripes, t is the counter for how many repeats will be done. fBufferSize and pBufferSize - gee, wonder what those do?

Reading in our .glkfont file to the buffer is pretty basic:


  assign(f,fontName+'.glkfont');
  reset(f,1);
    blockread(f,info,sizeof(info));
    fBufferSize:=filesize(f)-sizeof(info);  
    getmem(fBuffer,fBufferSize);
    blockread(f,fBuffer^,fBufferSize);
  close(f);

Open the file, read the header, subtract the header size from the filesize, allocate that much memory, and read the rest of the file into our buffer. Sure we could get fancy allocating a smaller buffer or using things like tStream - but when we are talking about allocating 400-500k for a pixel buffer, what difference does a temporary 25-30k file buffer make?

Some simple calculations are done next, as well as allocating our pixel buffer.


  { texture2f scale values }
  with info do begin
    charAddWidth:=(charWidth-1)/bufferWidth;
    charAddHeight:=(charHeight-1)/bufferHeight;
    pBufferSize:=trunc(bufferWidth)*trunc(bufferHeight)*2;
    getmem(pBuffer,pbufferSize);
  end;
  
  { vector scale values  }
  charGlHeight:=glHeight;
  scale(1);  

The reason we calculate charAdd[i]xxx[/i] as one less than it's reference, is that if we added the character width openGL would render the first pixel from the next character on each. Dividing by the buffer width scales it to openGL's default texture map coordinate range of 0..1. We pass the glHeight value here to so that our scaling routine can initialize everything. I use the scaling routine to initialize so we don't have the same code in two places. [i]That's what functions are FOR people![/i]

Initializing our pointers to iterate through our two buffers is next.


  fPoint:=fBuffer;
  fBufferEnd:=fPoint+fBufferSize;
  pPoint:=pointer(pBuffer);

Simple assignment. You'll notice that I typecase pPoint. Since it's currently ^word feeding it ^byte gives an error... we could typecast that as ^word, but if we change that type to say ^dword in the future for pre-colored/textured fonts, we'd have to remember to change that typecast. The generic 'pointer' type does not give typecast errors when assigned to complex pointers, removing that headache.

Now for the actual work of decoding our file format.


  while not(fpoint>=fBufferEnd) do begin
    case fPoint^ of 
      0,255:begin
        b:=$FF or (fPoint^ shl 8);
        inc(fPoint);
        t:=fPoint^;
        while (t>0) do begin
          pPoint^:=b;
          inc(pPoint);
          dec(t);
        end;
      end;
      else begin
        pPoint^:=$FF or (fPoint^ shl 8);
        inc(pPoint);
      end;
    end;
    inc(fPoint);
  end;

We loop through every byte in the datastream. If that byte is a 0 or 255 then we store that value as the high byte in our color word, setting full luminance as the low byte. We then increment the file pointer, read the next byte and repeat our word value in the pixel buffer by that amount. Meanwhile if the number is not 0 or 255, we just shift our value into the high byte, set the luminance and put it into the pixel buffer. In either case we will increment the file pointer to the next byte and keep looping.

At this point we're done with the file buffer so we can release that, and then bind our pixel buffer to OpenGL so we can release it too from the heap.


  freeMem(fBuffer);
  
  glGenTextures(1,@texture);
  glBindTexture(GL_TEXTURE_2D,texture);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);
  glTexParameteri (GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR_MIPMAP_LINEAR);
  gluBuild2DMipmaps(
    GL_TEXTURE_2D,2,
    info.bufferWidth,info.bufferHeight,
    GL_LUMINANCE_ALPHA,
    GL_UNSIGNED_BYTE,
    pBuffer
  );
  glBindTexture(GL_TEXTURE_2D,0); // disable it
  freeMem(pBuffer);

Pretty much that's a standard texture assignment, though as previously mentioned I use gluBuild2DMipmaps so that we get mipmaps, making resizing look a heck of a lot nicer. Once openGL has copied our pixel buffer I unbind the texture so that any future openGL calls won't mistakingly use our font as it's texture, then I free the pixel buffer.

Finally, I initialize just a coupe minor variables.


  renderX:=0;
  renderY:=0;
  charspacing:=3;
end;

renderX and renderY being our cursor position, and charspacing being the extra space added to our kerning. You can modify that value (I didn't think it warranted a function) to increase or decrease the font spacing as desired - just remember that value is calculated in original buffer pixels, NOT in OpenGL scale.

Next is our scale function.


  procedure tglKernedFont.scale(s:glFloat);
var
  t:word;
begin
  charScale:=(charGlHeight/info.charHeight)*s;
  charScaleWidth:=info.charWidth*charScale;
  charScaleHeight:=info.charHeight*charScale;
  for t:=0 to 5 do lastKern[t]:=0;
end;

The charScale variable is the most important here, but is pretty simple. Take the desired height of a character in the 'openGL' scale declared when you initialize the font and divide it by the pixel height of the actual character, then multiply by the scale. Pre-calculating the scaled width and height of a character makes our rendering a wee bit leaner. At this point I also empty the previous character kerning data, since if you change scale those values are likely no longer valid!

My setposition function is even more basic.


procedure tglKernedFont.setposition(x,y,z:glFloat);
var
  t:word;
begin
  renderX:=x;
  renderY:=y;
  renderZ:=z;
  for t:=0 to 5 do lastKern[t]:=0;
end;

Set the x, set the y, set the z, invalidate the kerning.

Next is the string size function. Since our variable widths are fluid, it helps to be able to find out how long a string is going to be in our openGL coordinates BEFORE you render it should you want to center or right justify the string, or use glTranslate to move to that location to render something else.


function tglKernedFont.stringSize(st:string):glFloat;
var
  kern:tglKernedChar;
  calcX,r:glFloat;
  t:word;
begin
  for t:=0 to 5 do kern[t]:=0;
  calcX:=0;
  if (length(st)>0) then begin
    for t:=1 to length(st) do begin
      calcX+=(
        (
          kernCompare(kern,info.kerningTable[ord(st[t])]) +
          charSpacing
        ) *  charScale
      );
      kern:=info.kerningTable[ord(st[t])];
    end;
    r:=0;
    for t:=3 to 5 do begin
      if kern[t]>r then r:=kern[t];
    end;
    calcX+=r*charScale;
  end;
  stringSize:=calcX;
end;

The Kern variable is our holder to be used instead of lastKern when calling our kernCompare function. CalcX is our renderX stand-in for calculating the width, r is a holder for doing the final letter comparison by hand instead of calling our routine. t is of course a throwaway counter.

First we empty kern, set calcX to zero. We then make sure there even is a string value to check, then iterate through the string adding up the kerning of each character, scaling it as appropriate. At the end of each character we set kern to the current character, so we can access it's info on the next loop. Past the loop we then go through that last bit of kerning info so we can add the last rendered character's width to the total, again scaling it to size. Return the value and move on to our next function...

Writing a character.


procedure tglKernedFont.writeChar(ch:char);
var
  vsX,vsY,veX,veY,
  tsX,tsY,teX,teY:glFloat;
begin
  case ch of
    #32..#127:begin
      {  scale the character positions to 0..1  }
      tsX:=(ord(ch) mod 32)/32; 
      tsY:=((ord(ch) div 32)-1)/3;
      teX:=tsX+charAddWidth;
      teY:=tsY+charAddHeight;
      
      { perform kerning }
      renderX+=(
        kernCompare(lastKern,info.kerningTable[ord(ch)]) +
        charSpacing
      )*charScale;
      
      vsX:=renderX;
      vsY:=renderY;
      veX:=renderX+charScaleWidth;
      veY:=renderY+charScaleHeight;
      
      glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);
      glBindTexture(GL_TEXTURE_2D,texture);
      glBegin(GL_QUADS);
        glTexCoord2f(tsX,tsY);
        glVertex3f(vsX,vsY,renderZ);
        glTexCoord2f(teX,tsY);
        glVertex3f(veX,vsY,renderZ);
        glTexCoord2f(teX,teY);
        glVertex3f(veX,veY,renderZ);
        glTexCoord2f(tsX,teY);
        glVertex3f(vsX,veY,renderZ);
      glEnd;
      glBindTexture(GL_TEXTURE_2D,0);
      
      lastKern:=info.kerningTable[ord(ch)];
    end;
  end;
end;

While it's a good number of lines, it's not that complex. We have the vector x coordinates and texture x coordinates. First we make sure the character we're trying to render is valid to the range of our fonts, and so we calculate the texture coordinates then perform our kerning on renderX so we can calculate the vector coordinates. Get the right blending mode going, bind the texture, and actually draw the character. When done, just set up lastKern so that the next time this function is called it knows what character preceeded it.

Writing a string now that we can write a character is piss simple.


procedure tglKernedFont.writeString(st:string);
var
  t:word;
begin
  { use while so that zero length is unprocessed! }
  t:=0;
  while (t<length(st)) do begin
    inc(t);
    writeChar(st[t]);
  end;
end;

Loop through the string, write each character. Using while prevents the excess if statement we'd need with a for loop.

I've tossed in two additional string writing routines to make use of that stringsize function - one centers the text not just on the X axis, but on the Y axis as well. The other right justifies the text but leaves the Y positioning based off the top.


procedure tglKernedFont.writeStringCentered(st:string);
begin
  renderX-=stringSize(st)/2;
  renderY-=(info.charHeight*charScale)/3; // one third == center of capsline and baseline
  writeString(st);
end;
  
procedure tglKernedFont.writeStringRight(st:string);
begin
  renderX-=stringSize(st);
  writeString(st);
end;

Both of these functions just do some math and positioning, then hand off to regular writestring function. On the centering function I actually use a third of the total font size for this since the bottom third of the image is typically the descender space, apart from that neither of these functions is too complex.

Finally, we clean things up.


destructor tglKernedFont.term;
begin
  glDeleteTextures(1,@texture);
end;

While technically you shouldn't have to manually delete the textures, I do so anyways in case you do decide to do so - handy if you decide you want a font on the menu, but want to reclaim that openGL texture memory for in-game.

... and that's it. The full code as of version 0.9. I'll TRY to keep this up to date as I add new features, but I make no promises.

  1. Top of Page
  2. Main Menu
  3. Page Content
  4. Page Extras
  5. Bottom of Page