As you maybe noticed in my previous screenshots, there is some font rendering going on. I must admit that I have not performed any experimenting or testing various techniques for rendering font, but the way I’m doing it now seems to be fairly decent. At least I’m not getting any notable reduction in framerate when rendering my 20 letters :]. Let’s move on to more important matters, how is this done? I was quite disappointed when I noticed that DirectX had dropped font rendering in D3D11 and that I basically was forced to write my own or rely on GDI. I thought GDI sounded a bit scary and since I knew about the excellent FreeType font library I gave custom font rendering a shot. As it turns out, it was pretty easy with the help of FreeType. It uses pixel positioning and sizing but I’m not 100% convinced that this works as it should, the text seems to change size when I change the resolution. The purpose of me posting this code is mostly so other can see the general idea on how to get started with custom font rendering, and I would probably not recommend just copying and pasting it. However, you can copy and paste it if you like, since it’s licensed under the BSD license. Hopefully, someone will find the code useful.
Below is practically all code I use for C++-side font rendering. I will try to write comments for most of the parts but I’m usually quite greedy with comments so if you read this and have questions – don’t hesitate to ask! The code below is not very thoroughly tested, e.g. while I was reading it as I commented it I spotted no less than two memory leak (hopefully fixed now), so take care while using it. The most important high-level function is placed in the end. Also, I really recommend reading the freetype tutorial. Once again, I’m sorry about the formatting, wordpress ruined it for me when it was almost done.
////////LICENSE STUFF
Copyright (c) 2011, Daniel Lindén
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution.
* Neither the name of the project nor the names of its contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//////// HEADER FILE
//Data used for information about all glyphs
//A glyph is a bitmap with one character in it
struct GlyphData
{
Vector2 start;
Vector2 end;
Vector2 size;
unsigned int advance;
int xOffset;
int yOffset;
};
//A static string whose content can't be modified
class StaticString
{
friend class FaceMap;
public:
void Render(int xpos, int ypos);
protected:
std::tr1::shared_ptr m_pVerts;
FaceMap* m_pFace;
Device* m_pDevice;
};
//To be implemented
class DynamicString : public StaticString
{
friend class FaceMap;
public:
private:
};
//A FaceMap is a class which contains a texture with all of the glyphs, it is also the class used for creating renderable strings of text
class FaceMap
{
friend class FontManager;
public:
StaticString BuildStaticString(const std::string& text, const Vec3& color, unsigned int maxWidth = 0);
DynamicString BuildDynamicString(unsigned int maxStringLength, unsigned int maxWidth = 0, bool lineBreak = false);
unsigned int GetStringWidth(std::string text);
unsigned int GetAdvance(char c1, char c2);
TexResource GetTexture() { return m_TexResource; }
private:
FaceMap() {}
FT_Face m_Face;
Device* m_pDevice;
unsigned int m_Size;
TexResource m_TexResource;
std::map m_Coords;
};
//A font manager can be used for creating FaceMaps
class FontManager
{
public:
FontManager() : m_LastError(0) {}
bool Initialize(Device* pDevice);
bool LoadFont(std::string fontName);
std::vector GetAvailableFonts() const;
FaceMap* CreateFace(std::string familyName, std::string styleName, unsigned int size);
private:
FT_Library m_Library;
std::multimap m_Faces;
std::map, FaceMap*> m_FaceMaps;
Device* m_pDevice;
const char* m_LastError;
};
/////////// CPP FILE
//The vertex type used for rendering fonts
struct FontVertex
{
Vector2 position;
Vector2 size;
Vector2 startUV;
Vector2 endUV;
static const D3D11_INPUT_ELEMENT_DESC* GetLayoutDesc(int& count)
{
count = 4;
static const D3D11_INPUT_ELEMENT_DESC pDesc[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"POSITION", 1, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
return pDesc;
}
};
//Just rendering the vertex buffer in the usual fashion
void StaticString::Render(int xpos, int ypos)
{
Matrix4 world = Matrix4::CreateTranslation(xpos, ypos, 0.f);
Material mat;
mat.m_Decal = m_pFace->GetTexture();
m_pDevice->GetDeviceContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
m_pDevice->SetObjectProperties(world, mat);
m_pDevice->SetVertexBuffer(m_pVerts);
m_pDevice->SetLayout();
m_pDevice->Draw();
m_pDevice->GetDeviceContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
}
//A helper function that converts a string into a vector that contains all the lines, where each line is a vector of all words
//Sidenote: is there really not a split function for strings in C++?!
std::vector> SplitToLinesOfWords(std::string text)
{
//Inner vector contains all words of a line, outer contains all lines.
std::vector> words;
words.push_back(std::vector());
std::string lastWord = "";
for (int i = 0; i second.advance;
int penX = 0;
int penY = 0;
std::vector vertices;
//Nothing special going on here, I guess
for (auto i = words.begin(); i != words.end(); ++i)
{
for (auto j = i->begin(); j != i->end(); ++j)
{
//Check if a new linebreak should be performed due to line overflow
if (maxWidth && GetStringWidth(*j) + penX > maxWidth)
{
penX = 0;
penY += m_Face->height;
}
for (int c = 0; c length(); ++c)
{
char c1 = j->at(c);
auto dataIt = m_Coords.find(c1);
if (dataIt != m_Coords.end())
{
GlyphData data = dataIt->second;
FontVertex v;
v.position = Vector2(penX + data.xOffset, penY + data.yOffset);
v.size = data.size;
v.startUV = data.start;
v.endUV = data.end;
vertices.push_back(v);
char c2 = (j->length() - 1) - c > 0 ? j->at(c + 1) : ' ';
penX += GetAdvance(c1, c2);
}
}
penX += spaceWidth;
}
penX = 0;
penY += m_Face->height;
}
StaticString str;
str.m_pVerts = VertexBuffer::CreateStatic(m_pDevice, &vertices[0], sizeof(FontVertex), vertices.size());
str.m_pFace = this;
str.m_pDevice = m_pDevice;
return str;
}
//Calculates the total width of a given string, assuming no line breaks
unsigned int FaceMap::GetStringWidth(std::string text)
{
unsigned int width = 0;
for (int i = 0; i second.size.x + 0.5f);
}
}
}
return width;
}
//Returns the advance between two characters
unsigned int FaceMap::GetAdvance(char c1, char c2)
{
auto it = m_Coords.find(c1);
if (it != m_Coords.end())
{
return it->second.advance;
}
return 0;
}
bool FontManager::Initialize(Device* pDevice)
{
m_pDevice = pDevice;
int error = FT_Init_FreeType(&m_Library);
if (error)
{
return false;
}
return true;
}
struct GlyphBitmap
{
int glyphIndex;
char* pGlyphBitmap;
unsigned int width;
unsigned int height;
int xOffset;
int yOffset;
int advance;
};
//Create a list of all glyps, with FreeType bitmaps, that is ready to be packed
std::vector RenderGlyphs(FT_Face f, std::string chars)
{
int error;
std::vector res;
res.reserve(chars.length());
FT_GlyphSlot slot = f->glyph;
for (int i = 0; i bitmap_left;
bmp.yOffset = -slot->bitmap_top;
bmp.width = slot->bitmap.width;
bmp.height = slot->bitmap.rows;
bmp.pGlyphBitmap = new char[bmp.width * bmp.height];
for (int y = 0; y < bmp.height; ++y)
{
for (int x = 0; x bitmap.buffer[x + y * slot->bitmap.pitch];
}
}
bmp.advance = slot->advance.x >> 6;
res.push_back(bmp);
}
return res;
}
//Do some clean up
void FreeGlyphBitmaps(std::vector& bmps)
{
for(auto i = bmps.begin(); i != bmps.end(); ++i)
{
delete [] i->pGlyphBitmap;
}
}
struct GlyphPos
{
unsigned int x;
unsigned int y;
};
struct PackedGlyphs
{
std::vector positioning;
unsigned int textureWidth;
unsigned int textureHeight;
};
//Pack the glyphs into a texture, only actual positioning will be calculated here
PackedGlyphs PackGlyphs(std::vector& glyphs, int maxTextureWidth = 64)
{
//Not using a sophisticated packing algorithm... yet. Hopefully this one turns out to be
//sufficiently good, glyphs are after all quite easy to pack.
PackedGlyphs pg;
pg.positioning.reserve(glyphs.size());
int currentX = 0;
int currentY = 0;
unsigned int lineMaxSize = 0;
for (auto i = glyphs.begin(); i != glyphs.end(); ++i)
{
if (currentX + i->width + 1 >= maxTextureWidth)
{
currentX = 0;
currentY += lineMaxSize + 1;
lineMaxSize = i->height;
}
GlyphPos data;
data.x = currentX;
data.y = currentY;
currentX += i->width + 1;
lineMaxSize = Max(i->height, lineMaxSize);
pg.positioning.push_back(data);
}
pg.textureWidth = maxTextureWidth;
pg.textureHeight = currentY + lineMaxSize + 1;
return pg;
}
//Just a helper function for copying a glyph into a full texture
void CopyGlyphBitmap(char* dest, unsigned int destPitch, GlyphBitmap src, GlyphPos dstPos)
{
for (int y = 0; y < src.height; ++y)
{
for (int x = 0; x < src.width; ++x)
{
dest[dstPos.x + x + (dstPos.y + y) * destPitch] = src.pGlyphBitmap[x + y * src.width];
}
}
}
//Creates the actual font texture
std::tr1::shared_ptr CreateFontTexture(Device* pDevice, PackedGlyphs& pg, std::vector& bmps)
{
char* textureData = new char[pg.textureHeight * pg.textureWidth];
ZeroMemory(textureData, pg.textureHeight * pg.textureWidth);
for (int i = 0; i < bmps.size(); ++i)
{
CopyGlyphBitmap(textureData, pg.textureWidth, bmps[i], pg.positioning[i]);
}
auto res = Texture::CreateFromData(pDevice, pg.textureWidth, pg.textureHeight, TEXTURE_FORMAT_8BIT_UNORM, textureData);
delete [] textureData;
return res;
}
//A function for converting our packed glyphs and bitmaps into the data that we actually need.
std::vector BuildGlyphData(PackedGlyphs& pg, std::vector& bmps)
{
std::vector gd;
gd.reserve(bmps.size());
for (int i = 0; i GetFile(fileName, RF_NORMAL);
if (!pRes)
{
m_LastError = "Invalid resource pointer";
return false;
}
FT_Face face;
int error = FT_New_Memory_Face(m_Library, (FT_Byte*)pRes->GetData(), pRes->GetSize(), 0, &face);
if (error)
{
m_LastError = "Unable to load font from file";
return false;
}
std::string familyName = face->family_name;
m_Faces.insert(make_pair(familyName, face));
int numFaces = face->num_faces;
for (int i = 1; i GetData(), pRes->GetSize(), i, &face);
if (error)
{
continue;
}
std::string familyName = face->family_name;
m_Faces.insert(make_pair(familyName, face));
}
return true;
}
FaceMap* FontManager::CreateFace(std::string familyName, std::string styleName, unsigned int size)
{
m_LastError = 0;
auto r = m_Faces.equal_range(familyName);
FT_Face f = nullptr;
for (auto i = r.first; i != r.second; ++i)
{
if (i->second->style_name == styleName)
{
f = i->second;
break;
}
}
if (!f)
{
//It could be a good idea to just take a random face or something here instead
m_LastError = "Unable to find specified font face";
return nullptr;
}
int error = 0;
error = FT_Set_Pixel_Sizes(f, 0, size);
if (error)
{
m_LastError = "Specified font size not supported by font";
return nullptr;
}
FaceMap* pFace = new FaceMap();
pFace->m_pDevice = m_pDevice;
pFace->m_Size = size;
pFace->m_Face = f;
std::string str = "ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖabcdefghijklmnopqrstuvwxyzåäö0123456789 ,.-!?$\"'";
auto bmps = RenderGlyphs(f, str);
auto packInfo = PackGlyphs(bmps);
auto glyphData = BuildGlyphData(packInfo, bmps);
auto pTexture = CreateFontTexture(m_pDevice, packInfo, bmps);
FreeGlyphBitmaps(bmps);
pFace->m_TexResource = m_pDevice->GetResources()->LoadTexture(pTexture);
for (int i = 0; i m_Coords.insert(std::make_pair(str.at(i), glyphData[i]));
}
return pFace;
}
There are lots of code uncommented, but most parts of it should be fairly easy to follow. I have been trying to stick with classic C functions for most of the parts, as I have started to like plain C functions more and more lately. Maybe it is because of that course in functional programming (haskell, to be precise – a really cool language by the way) I took last semester, I can really recommend looking into some functional language if you haven’t done it before. It certainly looks really strange in the beginning, but once you get used to it you really miss stuff like map, for example.
Here is the shader code, with some constants undefined (I have them in my common header file, just replace them):
////// VERTEX SHADER
struct VS_INPUT
{
float2 pos : POSITION0;
float2 size : POSITION1;
float2 uvStart : TEXCOORD0;
float2 uvEnd : TEXCOORD1;
};
VS_INPUT main(VS_INPUT input)
{
VS_INPUT output;
output.pos = mul(float4(input.pos, 0.f, 1.f), World).xy * InvResolution * 2.f - float2(1.f, 1.f);
output.pos.y = -output.pos.y;
output.size = input.size * InvResolution * 2.f;
output.size.y = -output.size.y;
output.uvStart = input.uvStart;
output.uvEnd = input.uvEnd;
return output;
}
////// GEOMETRY SHADER
struct GS_INPUT
{
float2 pos : POSITION0;
float2 size : POSITION1;
float2 uvStart : TEXCOORD0;
float2 uvEnd : TEXCOORD1;
};
struct GS_OUTPUT
{
float2 uv : TEXCOORD0;
float4 pos : SV_Position;
};
[maxvertexcount(4)]
void main(point GS_INPUT input[1], inout TriangleStream stream)
{
float2 pos = input[0].pos;
float2 size = input[0].size;
float2 uvBegin = input[0].uvStart;
float2 uvEnd = input[0].uvEnd;
GS_OUTPUT output0, output1, output2, output3;
output0.pos = float4(pos, 1.0f, 1.0f);
output0.uv = uvBegin;
output1.pos = float4(pos.x + size.x, pos.y, 1.0f, 1.0f);
output1.uv = float2(uvEnd.x, uvBegin.y);
output2.pos = float4(pos.x, pos.y + size.y, 1.0f, 1.0f);
output2.uv = float2(uvBegin.x, uvEnd.y);
output3.pos = float4(pos + size, 1.0f, 1.0f);
output3.uv = uvEnd;
stream.Append(output0);
stream.Append(output1);
stream.Append(output2);
stream.Append(output3);
stream.RestartStrip();
}
///////// PIXEL SHADER
float4 main(float2 uv : TEXCOORD0) : SV_Target0
{
return float4(Decal.Sample(Bilinear, uv).rrr, 1.f);
}
Oh, and a suggested to-do-list for the font renderer (depending on requirements, of course):
- Colored text
- Dynamic strings
- Using some sort of markup for creating partly colored strings, etc.
~Daniel