Building a 3D Engine in Perl, Part 4
by Geoff Broadwell
|
Pages: 1, 2, 3, 4, 5, 6, 7, 8
Speaking OpenGL's Language
The bitmaps produced by read_font_file are simply packed
bitstrings, in this case 11 bytes long (one byte per seven-pixel row). Before using
them to render strings, the engine must first load these bitmaps into OpenGL.
This happens in the main init_fonts routine:
sub init_fonts
{
my $self = shift;
my %fonts = (
numbers =& 'numbers-7x11.txt',
);
glPixelStore(GL_UNPACK_ALIGNMENT, 1);
foreach my $font (keys %fonts) {
my ($bitmaps, $w, $h) =
$self->read_font_file($fonts{$font});
my @cps = sort {$a <=& $b} keys %$bitmaps;
my $max_cp = $cps[-1];
my $base = glGenLists($max_cp + 1);
foreach my $codepoint (@cps) {
glNewList($base + $codepoint, GL_COMPILE);
glBitmap($w, $h, 0, 0, $w + 2, 0,
$bitmaps->{$codepoint});
glEndList;
}
$self->{fonts}{$font}{base} = $base;
}
}
init_fonts opens with a hash associating each known font with a
font file; at the moment, only the numbers font is defined. The
real work begins with the glPixelStore call, which tells OpenGL
that the rows for all bitmaps are tightly packed (along one-byte boundaries)
rather than being padded, so that each row begins at even two-, four-, or eight-byte
memory locations.
The main font loop starts by calling read_font_file to load the
bitmaps for the current font into memory. The next line sorts the codepoints
into @cps, and the following line finds the maximum codepoint by
simply taking the last one in @cps.
The glGenLists call allocates display lists for codepoints 0
through $max_cp, which will have numbers from $base
through $base + $max_cp. For each codepoint defined by the font,
the inner loop uses glNewList to start compiling the appropriate
list, glBitmap to load the bitmap into OpenGL, and finally,
glEndList to finish compiling the list.
The glBitmap call has six parameters aside from the bitmap
data itself ($bitmaps->{$codepoint}). The first two are the
width and height of the bitmap in pixels, which read_font_file
conveniently provides. The next two define the origin for the bitmap,
counted from the lower-left corner. Bitmap fonts use a non-zero origin for
several purposes, generally when the glyph extends farther left or below the
"normal" lower-left corner. This may be because the glyph has a
descender (a part of the glyph that descends below the general line
of text, as with the lowercase letters "p" and "y"), or perhaps because the font
leans to the left. The simple code in init_fonts assumes none of
these special cases apply and sets the origin to (0,0).
The last two parameters are the X and Y increments, the distances that OpenGL should move along the X and Y axes before drawing the next character. Left-to-right languages use fonts with positive X and zero Y increments; right-to-left languages use negative X and zero Y. Top-to-bottom languages use zero X and negative Y. The increments must include both the width/height of the character itself and any additional distance needed to provide proper spacing. In this case, the rendering will be left to right. I wanted two extra pixels for spacing, so I set the X increment to width plus two, and the Y increment to zero.
The last line of the outer loop simply saves the list base for the font to make it available later during rendering.
init calls init_fonts as usual, just after the
call to init_time:
$self->init_fonts;
Text Rendering
The hard part is now done: parsing the font file and loading the bitmaps
into OpenGL. The new draw_fps routine calculates and renders the
frame rate:
sub draw_fps
{
my $self = shift;
my $base = $self->{fonts}{numbers}{base};
my $d_time = $self->{world}{d_time} || 0.001;
my $fps = int(1 / $d_time);
glColor(1, 1, 1);
glRasterPos(10, 10, 0);
glListBase($base);
glCallListsScalar($fps);
}
The routine starts by retrieving the list base for the numbers
font, retrieving the world time delta for this frame, and calculating the
current frames per second as one frame in $d_time seconds. It takes
a little care to make sure $d_time is non-zero, even if the engine
is running so fast that it renders a frame in less than a millisecond (the
precision of SDL time handling); otherwise, the $fps calculation
would die with a divide-by-zero error.

