Font Metrics & DAT File Format
By Geep 2024
Overview
TDM provides a number of fonts (Fonts_Screenshots) that can be used, in conjunction with GUI code, on in-game surfaces, as HUD elements and messages, and subtitles. (There are additional aspects of menu and console fonts not covered here.)
While it is possible to design a TDM bitmap font from scratch, historically, a number of such fonts were built by starting from a TrueType Font. That outline-style font was not read into the engine directly. Instead, it was rendered into a set of bitmaps externally, with a conversion tool, and stored as DAT metric and TGA (really, DDS) bitmap image files. Commonly, there were post-conversion minor or major adjustments. More about this process is described in Font Conversion & Repair.
Once deployed, font files are then read by the engine at startup. In response to any specific GUI request, the engine applies the required character selection, scaling, transformation, and on-screen layout. While bitmap fonts are perhaps not optimal for font quality, compared to outline fonts, this approach had some likely advantages:
- In early Doom days, it no doubt avoided a performance hit if rendering from font outlines.
- It avoided dependency upon particular fonts pre-existing on the user’s system.
- It did not force use of TrueType; fonts could be created by other means.
- Even if started by a TrueType conversion, font characters could be made accessible for further adjustment with text and bitmap editors.
- It exposed the TGA/DDS to character customization, including to support a wider set of European and Russian languages. (See TDM-specific codepoint mappings I18N - Charset.)
Be aware that the idTech4 system doesn’t support kerning (i.e., adjustment of the spacing between any two characters to accommodate their shape particularities).
TDM Font Metrics
Figure 1. An abstract view of two typical character glyphs, like those found in any of a font's DDS files.
To understand the Doom3/idTech4/TDM font metrics, let’s start with a simplified view into a DDS bitmap [Figure 1], which is always 256 x 256 pixels in size and holds multiple character glyphs. With the exception of "baseline", all the named items shown comprise the fields associated with each character’s DAT entry. Most fields (those shown around the "g" image) are distances measured in typically-positive integer pixels. Corner coordinates (shown around the "J" image) are reported in 1/256 units, as a decimal value with 6-digit precision.
In the figure, the black-outlined box represents a glyph's defined bitmap, while the gray zone represents the tight rectangular bounding box around the glyph, covering all its pixels that have non-zero alphas. In this example, both glyphs here show a small padding in white (e.g., of 1 pixel size) around 3 sides. Portrayed is a somewhat bigger padding on the left, which is not uncommon.
Of particular importance, there are two red dots on the baseline, controlling where the character is ultimately laid out on a line. The left dot, the "origin", is defined by a positive "pitch" distance (also known more aptly in some tools as xOffset). For most characters, there is typically a gap between the origin and the gray bounding box (e.g., shown let’s say as 1 pixel for "g"). If a descender (in the "bottom" region) needs to be under a preceding character, as in the "J" example, pitch can arrange that as shown.
The right dot, located relative to the origin dot by a positive "xSkip", will determine where the origin of the next character in-line will be placed. The right dot is shown in this figure as aligned with the right side of the defined bitmap, which (given right-side padding) is usually the case but not always. For instance, if the font had a tall version of “{“, you might want xSkip to be slightly less to bring the next rendered character within its embrace. Conversely, xSkip could in theory place the dot beyond the imageWidth boundary.
The details of the padding size can vary, depending on how the DDS was originally constructed (e.g., with certain options of a particular TrueType conversion tool) and subsequently altered. There may be no padding on the bottom, top, and right side, particularly for 12pt font. Or more padding for 48pt font. Or the top padding for lower-case characters "g" may be very much enlarged to match that for upper-case characters like "j".
In a well-formed DAT file, "height" and "imageHeight" of a given character will always have the same value. (Inside the engine, there are other uses for this data structure, where a difference between imageHeight and height can indicate scaling. Similarly, internally, the "pitch" field can take on a different meaning: to indicate an optimized bitmap-transfer width in bytes. Perhaps the name "pitch" makes more sense in that context.)
The DAT File Format
A DAT file is a fixed-size binary file. It begins with 256 blocks of 80 bytes each. There is one block for each glyph, in assumed ascending codepoint order. Each block contains this layout:
int height int top int bottom int pitch int xSkip int imageWidth int imageHeight float s float t float s2 float t2 int glyph // placeholder, simply zero char[32] shaderName // DDS filename for this specific glyph; see description below.
At the end of the file, there is:
float glyphScale char[64] name // this file's TDM path and name but without DAT extension, e.g., "fonts/fontImage_24"
The ints here are each 4 bytes in size, e.g., 32 bits, not 64. Floats are also 4 bytes, as is normal. (When ultimately read into the engine, the integers will end up as floats, in a more general data structure font_info_t.)
Understanding the S, T, S2, and T2 Values
In the DAT file, a given character’s s, s2, t, and t2 are all float values in the inclusive range [0.00000 to 1.000000]. Values outside this range can cause anomalous rendering. Float precision implies, given that there’s only one digit to the left of the decimal point, that 6 digits after the decimal point should have meaningful content (not just zeros or junk).
Best practice is to set these values as equal to n / 256.0, where n is an integer in the inclusive range [0 to 256]. This is what ExportFontToDoom3 does when first generating a DAT file; Font Patcher and the REF file format of Refont also maintain this.
But the FNT file format of Q3Font allows arbitrary values to be set, so it’s the Wild West. This is regrettably reflected in some actual DAT files. Fortunately, the engine has tolerance for squishy values and provides its own rounding. Nevertheless, when changing these values, it is recommended you provide an n' / 256.0 to full precision, to minimize risk of rounding errors.
s2 >= s and t2 >= t // all are zero if codepoint is skipped
It is perhaps surprising that 256 instead of 255 is used as the divisor. There are two equivalent ways to think about this:
The bitmap, of size 256 x 256, can be visualized as a grid of square pixels. Consider the grid lines (not the pixels) as numbered 0 to 256. Then (s,t) and (s2,t2) are the grid points defining the overall sub-bitmap of interest.
Alternatively, given a pixel array "pix[nx,ny]", then pix[s,t] defines the upper-left pixel to include in the desired sub-bitmap, while pix[s2,t2] defines the lower-right pixel to exclude. In this formulation, (s2, t2) of floating representation (1.0, 1.0) becomes pix[256,256], in a "ghost" column and row outside the actual array.
Other Expected DAT Values
Here is what you should expect in a well-formed DAT file.
The "glyphScale" at the end has one of three values, which presumably ties the font data into a GUI’s "textscale" value:
- For 48 pt: 1.000000
- For 24 pt: 2.000000
- For 12 pt: 4.000000
For Each Glyph
Integer values are generally in the inclusive range [0 to 256]. There are occasional exceptions, e.g., a negative value of "top" for underscore, that is entirely below the baseline.
The meaning and best practices for float value s, s2, t, and t2 were described in the previous section.
s2 >= s and t2 >= t // all are zero if codepoint is skipped
A codepoint for which no unique glyph is provided is typically directed to either a space, a hollow box glyph, or zero-dimension image. Or to a substitute glyph; for instance, an ASCII unaccented character may stand-in for a missing European accented character.
Other expectations:
imageWidth = round(s2 * 256) - round(s * 256) // where “round” forces to nearest integer value. imageHeight = round(t2 * 256) - round(t * 256) imageHeight = height
Ideally, this would also be a requirement:
height = top + bottom
However, in practice, bottom is not used within the engine, and so, being informational only, is often incorrect in the DAT file. Instead, the engine relies on:
baseline = height - top
Also, as discussed above:
pitch + xSkip approximately equals imageWidth
Finally: "glyph" is left zeroed; inside the engine, it becomes a pointer to the in-memory version of the data.
"shaderName" is a TDM-style relative path to the specific DDS file containing the glyth. It always begins with "fonts/" and gives the extension as ".tga", even for DDS. Example shaderName:
fonts/Stone_0_24.tga
(When the engine reads this, internally the "fonts/" prefix is stripped off.)
More about the DDS File Format
See
- Font Conversion & Repair for how to view and adjust DAT values, as well as important constraints of changing metrics on long-deployed fonts.
- Font Files for DDS file naming, directory location, scaling, and usage by TDM.
- Font Bitmaps in DDS Files about color and alpha representations, with examples.