使用freeglut渲染英文字体有个非常简单的办法,那就是直接调用glutBitmapCharacter函数进行渲染。代码如下:
//要显示的字符
char str[20] = "Hello world!!!";
int n = strlen(str);
//设置要在屏幕上显示字符的起始位置
glRasterPos2i(0, 0);
//逐个显示字符串中的每个字符
for (int i = 0; i < n; i++)
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *(str + i));
然而glutBitmapCharacter函数无法设置字体样式,无法改变字体大小,最要命的还有它无法使用中文字体。
解决上面提到的这些问题,最简单的方法是读取字体文件到内存中,然后生成纹理进行贴图。
这一过程有两种实现方法:
其一与操作系统相关,利用操作系统的API来读取字体文件。该实现方法的好处是代码量比较小,而且不用自行配置别的库文件。缺点则是无法跨平台。
其二与操作系统无关,利用FreeType开源库来加载字体到内存中。该实现方法相对而言代码量要多一点,而且多了配置FreeType库的步骤。好处则是可以跨平台。
由于考虑到大部分的初学者是在Windows上面学习的,因此本教程采用了第一种实现方法。如对第二种实现方法有兴趣的可以去看看LearnOpenGL的教程,该教程详细的介绍了如何在glfw库中使用FreeType显示英文字符(中文类似),同时也做了字符渲染原理的详细介绍,地址如下:
https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/
接下来简要的讲一下实现字体渲染需要注意的点:
- 无论使用操作系统的API还是使用FreeType来读取字体到内存中,都是从文件读取到内存的过程,如果文字量很大或者是反复读取,会严重的影响程序的性能。因此在具体实现的时候,最好将已经读取过的文字放入一张映射表中,然后在渲染的时候查询该表来判断是否需要进行字符的读取,以避免反复的字符读取操作。当然这样做如果文字量很大会有内存占用过大的问题,需要想办法进行优化,不过对于一般应用而言无需担心。
- 字符读取到内存中后是一个nxm的方块,在方块的内部0值代表这个像素没有点,而其他值则代表该点是构成文字的一个像素。一般在文字内部,如果是64阶的话,那么文字所占的点多为64,而在文字的边沿处则存在着更小的值,这些值是模糊过度的值,不可忽略,否则文字渲染出来将存在很多锯齿,对比图如下(这也是网上很多给出代码的demo看着很别扭的原因)。
到此我们就将中文文字渲染需要注意的点讲完了,如果还想更深入了解文字渲染的细节可以阅读下面放出来的代码,或者去上面提到的LearnOpenGL教程看看。
OK,下面放出本人封装好的代码(部分参考了网上别的博主给出的代码,本来要上链接,结果发现不知道当时从哪里找到的。。。如发现有雷同可以告知我我加上。需要强调的是,本人对该代码做了很多修改和注释,绝非复制粘贴):
Font.hpp
#pragma once
#include <Windows.h>
#include <gl/GL.h>
#include <gl/GLU.h>
#include <map>
#include <math.h>
class CFontData
{
public:
float m_Width, m_Height;
float m_OrigX, m_OrigY;
float m_FontWidth;
GLuint m_TextureID;
CFontData()
{
m_Width = 0.0f;
m_Height = 0.0f;
m_TextureID = 0;
m_FontWidth = 0.0f;
m_OrigX = 0.0f;
m_OrigY = 0.0f;
}
};
class CFontPrinter
{
private:
int m_FontSize;//字体大小
char m_FontName[64];//字体名称
std::map<wchar_t, CFontData*> m_FontMap;//文字映射表
HFONT m_Font;//字体对象
//字体颜色
unsigned char red;
unsigned char green;
unsigned char blue;
//背景颜色
unsigned char bg_r = 0;
unsigned char bg_g = 0;
unsigned char bg_b = 255;
//父窗口大小
float fwinwidth;
float fwinheight;
public:
CFontPrinter(int fontSize, const char* fontName,int fwwidth,int fwheight)
{
strcpy_s(m_FontName, 64, fontName);//获得字体名称
m_FontSize = fontSize;//获得字体大小
m_Font = NULL;
red = 255;
green = 255;
blue = 255;
fwinwidth = (float)fwwidth;
fwinheight = (float)fwheight;
}
bool makeChar(wchar_t wChar)//生成单个文字对象,并放入映射表中
{
HDC hdc = CreateCompatibleDC(wglGetCurrentDC());
if (!hdc)
return false;
HBITMAP hbitmap = CreateCompatibleBitmap(hdc, 1, 1);
HBITMAP hbitmapOld = (HBITMAP)SelectObject(hdc, hbitmap);
if ((DWORD)hbitmapOld == GDI_ERROR)
return false;
//CFontData* fontData = new CFontData();
m_FontMap[wChar] = new CFontData();
glGenTextures(1, &m_FontMap[wChar]->m_TextureID);//生成纹理,放入m_TextureID中
glBindTexture(GL_TEXTURE_2D, m_FontMap[wChar]->m_TextureID);//绑定生成的纹理
//设置纹理的拉伸收缩方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
//指定OpenGL如何从数据缓冲区中解包图像数据
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
int iTexWidth = m_FontSize;
int iTexHeight = m_FontSize;
int iLevel = 0;
if (!m_Font)//创建字体
{
LOGFONTA lf;
lf.lfHeight = -m_FontSize;
lf.lfWidth = 0;
lf.lfEscapement = 0;
lf.lfOrientation = 0;
lf.lfWeight = 5;
lf.lfItalic = 0; //斜体
lf.lfUnderline = 0; //下划线
lf.lfStrikeOut = 0;
lf.lfCharSet = DEFAULT_CHARSET;
lf.lfOutPrecision = 0;
lf.lfClipPrecision = 0;
lf.lfQuality = PROOF_QUALITY;
lf.lfPitchAndFamily = 0;
strcpy_s(lf.lfFaceName, m_FontName);
m_Font = CreateFontIndirectA(&lf);
if (!m_Font)//检查创建结果
return false;
}
HFONT hfontOld = (HFONT)SelectObject(hdc, m_Font);
if ((DWORD)hfontOld == GDI_ERROR)
return false;
GLYPHMETRICS gm = { 0, };//存储字符相关信息
MAT2 const matrix22_identity = { {0,1},{0,0},{0,0},{0,1} };
BYTE* pBuff = new BYTE[m_FontSize * m_FontSize];
BYTE* pSwapBuff = new BYTE[m_FontSize * m_FontSize * 3];
memset(pBuff, 0xff, m_FontSize * m_FontSize);
memset(pSwapBuff, 0xff, m_FontSize * m_FontSize * 3);
DWORD dwBuffSize;
if ((dwBuffSize = GetGlyphOutlineW(hdc, wChar, GGO_GRAY8_BITMAP, &gm, m_FontSize * m_FontSize, pBuff, &matrix22_identity)) == GDI_ERROR)
{
delete[] pBuff;
return false;
}
// 原本字体的灰度值为0~64,经过调整映射到0~255
unsigned int const uiRowSize = dwBuffSize / gm.gmBlackBoxY;
BYTE* pPtr;
BYTE* pSPtr;
for (unsigned int nY = 0; nY < gm.gmBlackBoxY; nY++)
{
pPtr = pBuff + uiRowSize * nY;
pSPtr = pSwapBuff + gm.gmBlackBoxX*3 * nY;
for (unsigned int nX = 0; nX < gm.gmBlackBoxX; nX++)
{
if (*pPtr == 0)//背景
{
*pSPtr = bg_r;
pSPtr++;
*pSPtr = bg_g;
pSPtr++;
*pSPtr = bg_b;
pSPtr++;
}
else if (*pPtr == 64)
{
*pSPtr = red;
pSPtr++;
*pSPtr = green;
pSPtr++;
*pSPtr = blue;
pSPtr++;
}
else
{
float time = (* pPtr)/(float)64;
int blend_r = unsigned char(red * time) + unsigned char(bg_r * (1 - time));
if (blend_r > 255)
{
blend_r = 255;
}
*pSPtr = blend_r;
pSPtr++;
int blend_g = unsigned char(green * time) + unsigned char(bg_g * (1 - time));
if (blend_g > 255)
{
blend_g = 255;
}
*pSPtr = blend_g;
pSPtr++;
int blend_b = unsigned char(blue * time) + unsigned char(bg_b * (1 - time));
if (blend_b > 255)
{
blend_b = 255;
}
*pSPtr = blend_b;
pSPtr++;
}
pPtr++;
}
//std::cout << std::endl;
}
// 设置纹理,将字体装入其中
// iLevel表示图像级别,GL_LUMINANCE表示亮度
glTexImage2D(GL_TEXTURE_2D, iLevel, GL_RGB, gm.gmBlackBoxX, gm.gmBlackBoxY, 0, GL_RGB, GL_UNSIGNED_BYTE, pSwapBuff);
/*glTexImage2D(GL_TEXTURE_2D, iLevel, GL_LUMINANCE8, gm.gmBlackBoxX, gm.gmBlackBoxY, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, pSwapBuff);*/
// 将字符对象加入到映射地图中,这样就不需要反复创建
// 字体所占宽度比例,比如设置9而所占96则最后得到的就是1
m_FontMap[wChar]->m_FontWidth = (float)gm.gmCellIncX / (float)m_FontSize;
// 字体实际宽度比例,一般获得的字体宽度要比我们设定的窄
m_FontMap[wChar]->m_Width = (float)gm.gmBlackBoxX / (float)m_FontSize;
// 字体实际高度比例
m_FontMap[wChar]->m_Height = (float)gm.gmBlackBoxY / (float)m_FontSize;
// 字体实际左边x坐标与设定字尺寸的比例,因为字实际高度比设定的要窄,因此这个起点就定义了实际上字是从哪个坐标上开始的
m_FontMap[wChar]->m_OrigX = (float)gm.gmptGlyphOrigin.x / (float)m_FontSize;
// 字体实际左边x坐标与设定字尺寸的比例
m_FontMap[wChar]->m_OrigY = 1.0f - (float)gm.gmptGlyphOrigin.y / (float)m_FontSize;
//m_FontMap[wChar] = fontData;
//释放对象
DeleteObject(hbitmapOld);
DeleteObject(hbitmap);
DeleteDC(hdc);
delete[] pBuff;
delete[] pSwapBuff;
return true;
}
void setFontColor(unsigned char r, unsigned char g, unsigned char b)//设置文字颜色
{
red = r;
green = g;
blue = b;
}
void setBGColor(unsigned char r, unsigned char g, unsigned char b)//设置背景颜色
{
bg_r = r;
bg_g = g;
bg_b = b;
}
void setfatherWinSize(int w, int h)//设置父窗口大小
{
fwinwidth = (float)w;
fwinheight = (float)h;
}
void print3DText(float x, float y,float width, const wchar_t* Text)//绘制文字
{
glColor3f(0xff / (float)255, 0xff / (float)255, 0xff / (float)255);//设置颜色空间为白色,否则会影响绘图
size_t iTextLenth = wcslen(Text);
//计算空格的个数
wchar_t space[] = L" ";//空格处理
int spacenum = 0;
for (size_t i = 0; i < iTextLenth; i++)
{
if (Text[i] == space[0])//处理空格
{
spacenum++;
}
}
//启用2D纹理
glEnable(GL_TEXTURE_2D);
//居中绘制文字
float fX = x + width / 2 - (spacenum+ (iTextLenth- spacenum) *2) * m_FontSize / 2 / 2;
float fY = fwinheight - y;
float fZ = 0;
for (size_t i = 0; i < iTextLenth; i++)
{
if (m_FontMap[Text[i]] == NULL && Text[i] != space[0])
{
makeChar(Text[i]);
}
// 绘制字体
CFontData* fontData = m_FontMap[Text[i]];
if (fontData)
{
glMatrixMode(GL_PROJECTION);//将当前矩阵指定为投影矩阵,以便进行投影操作
glLoadIdentity();//将操作矩阵设为单位矩阵
glOrtho(0, fwinwidth, 0, fwinheight, 0, 100);
float iWidth = fontData->m_Width * m_FontSize;
float iOrigX = fontData->m_OrigX * m_FontSize;
float iHeight = fontData->m_Height * m_FontSize;
float iOrigY = fontData->m_OrigY * m_FontSize;
glBindTexture(GL_TEXTURE_2D, fontData->m_TextureID);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(fX + iOrigX, fY - iOrigY - iHeight, fZ);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(fX + iOrigX, fY - iOrigY, fZ);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(fX + iOrigX + iWidth, fY - iOrigY, fZ);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(fX + iOrigX + iWidth, fY - iOrigY - iHeight, fZ);
glEnd();
fX += fontData->m_FontWidth * m_FontSize;
glLoadIdentity();// 重置当前的模型观察矩阵,消除投影矩阵调用带来的影响
}
else if(Text[i] == space[0])//处理空格
{
fX += m_FontSize/2;
}
}
glDisable(GL_TEXTURE_2D);
}
};
调用代码
#include <gl/freeglut.h>
#include "Font.hpp"
//窗口大小
int widows_width = 800;
CFontPrinter* font = NULL;
void display(void)
{
//设置背景颜色
glClearColor(0x00/float(255), 0x2C / float(255), 0x3E / float(255), 1.0);
glClear(GL_COLOR_BUFFER_BIT);//以设置清除窗口
//设置标题
font->print3DText(0, 10, widows_width, L"综 合 视 频 处 理 系 统");//第一个参数为x,第二个参数为y,第三个参数为宽度,坐标系x向右为正,y向下为正
glFlush();
//缓冲区翻转显示图像
glutSwapBuffers();
}
void changeSize(int w, int h)
{
widows_width = w;
glViewport(0, 0, w, h);//设定视口的大小
font->setfatherWinSize(w, h);//通知字体渲染对象窗口大小改变了
}
//UI初始化
void UIInit()
{
int ppi = 2;
font = new CFontPrinter(28 * ppi, "微软雅黑", 800, 600);//创建字体对象
font->setFontColor(0xF7, 0xF8, 0xF3);//设置字体颜色
font->setBGColor(0x00, 0x2C, 0x3E);//设置背景颜色
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);//设置绘图模式
UIInit();//初始化UI
glutInitWindowPosition(0, 0);//设置窗口弹出位置
glutInitWindowSize(1920, 1080);//设置窗口大小
glViewport(0, 0, 1920, 1080);//设定视口的大小
glutCreateWindow("imgshow");//创建窗口
glutDisplayFunc(&display);//定义绘图回调函数
glutReshapeFunc(changeSize);//注册窗口大小改变响应函数
glutFullScreen();//窗口全屏
glutMainLoop();//进入消息循环
return 0;
}