OpenGL中显示中文的解决方案多种多样,很多文章已经对这个问题进行了深入的研究。总结起来,中文在OpenGL渲染环境中的显示主要采用两种方法:
1、预先将一个或多个表示中文文字的二值位图绑定为纹理,然后在指定位置绘制;
2、将中文绘制为二值位图,采用glbitmap方法,实时地绘制。
方法1需要预先绑定纹理,或是将要显示的字体一个一个绑定,在绘制时拼成一段文字,或是将一段文字绑定为一个纹理绘制。方法1比较简单,但相对而言不是非常灵活,例如,当程序运行时要在屏幕上输出某个没有绑定为纹理的文字时,就无法显示。方法2可以实时地改变需要绘制的文字,因为没有预先绑定纹理,这种方法可以在程序运行时随意改变显示的文字内容。事实上,方法2也更多地被采用。两种方法相同之处是,均采用绘制二值位图方式在OpenGL下绘制,显示的字体比较难以表现出更多样式,如描边的效果。Freetype2中给出了描边中文字体的解决方案,但其实现途径似乎比较复杂。
GDI+为中文字体的绘制提供了很好的解决方案,使用比较简单,能够很容易地和OpenGL结合起来绘制中文字体的各种效果。这里以一个单文档的MFC程序为例,把这种解决方案各个步骤归纳一下。
一、GDI+绘制描边字体
(1)使用GDI+,在视图类头文件中加入如下代码,启用GDI+。
#include <GdiPlus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
并声明变量
Gdiplus::GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;
(2)在程序初始化时,启动GDI+。例如,在视图类的构造函数中加入:
Gdiplus::GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
(3)在程序结束时,关闭GDI+。同样,可以在视图类的析构函数中加入:
Gdiplus::GdiplusShutdown(m_gdiplusToken);
(4)用GDI+绘制描边字体
为视图类添加函数
GLuint GenerateFontTex_Outline( CString strFont, CString strContent, bool bShear, Color fontcolor, Color OutlineColor, float fSize, FontStyle sFontStyle)
函数返回一个绑定的纹理ID。其中的参数分别代表:
strFont:字体名称,宋体、黑体、还是其他
strContent:文字内容
bShear:是否反斜体
fontcolor:字体颜色
OutlineColor:描边颜色
fSize:字体大小
sFontStyle:字体风格:正常、加粗、斜体、下划线等等
函数中实现两个主要的功能,一是使用GDI+绘制描边字体到位图,二是利用该位图资源生成一个纹理,返回一个纹理ID。
一、使用GDI+绘制字体:
//*****************************************************************//
GraphicsPath path;//定义一个图形路径,用来绘制
Size RectBound;//定义要创建的位图尺寸
StringFormat strformat;//字符串的格式
strformat.SetAlignment(StringAlignmentNear);//设置为左对齐,为了准确地量测字符串的长度和宽度
WCHAR *chName;
USES_CONVERSION;
chName = A2W( strContent.GetBuffer(0));//将传入的字符串转换为WCHAR类型
Gdiplus::FontFamily fontFamily(strFont.AllocSysString()//);定义FontFamily,这里指定了要采用的字体类型
path.AddString( chName,-1,&fontFamily,sFontStyle,fSize,PointF(0,0),&strformat);//将字符串加入图形路径中,这里指定了字符串、字体、字体样式、字体大小和对齐方式,为了准确地量测字符串的长宽
RectF rcBound;
path.GetBounds(&rcBound);//得到图形路径的大小,就是我们要创建的位图的大小
RectBound.Width = int(rcBound.Width + rcBound.X);
RectBound.Height = int(rcBound.Height + rcBound.Y);
RectBound.Width = (int(RectBound.Width) + 31) & (~31);
RectBound.Height = (int(RectBound.Height) + 31) & (~31);
//创建一个带Alpha通道的位图
PixelFormat PF;
PF = PixelFormat32bppARGB;
Bitmap bmp( RectBound.Width, RectBound.Height, PF );
BitmapData * pBitmapData = new BitmapData;
//在该位图上绘制字体
Graphics graphic(&bmp);
graphic.SetSmoothingMode(SmoothingModeAntiAlias);
graphic.SetInterpolationMode(InterpolationModeHighQualityBicubic);
//判断是否要反斜体绘制,如果是,则将图形的矩阵乘以一个切变矩阵,以实现斜体字的绘制
if ( bShear )
{
Matrix matrix;
matrix.Shear( 0.5,0 );
graphic.MultiplyTransform( &matrix );
}
//定义GDI+字体绘制的大小,与位图等大
RectF TitleRc(0, 0, (REAL) RectBound.Width, (REAL) RectBound.Height);
//绘制外围轮廓
Pen pen( OutlineColor, Gdiplus::REAL( fSize / 8.0));
graphic.DrawPath(&pen, &path);
////绘制字体
SolidBrush brush(fontcolor);
graphic.FillPath(&brush, &path);
bmp.RotateFlip( Rotate180FlipX );//这里记得要绕X轴旋转180度,因为位图的存储是上下翻转的;当然,也可以不在这里翻转,只是在绘制的时候需要让纹理坐标与顶点坐标的顺序反向。
//*****************************************************************//
至此,我们已经在一个创建的、带有Alpha通道的位图上绘制上了我们要的字符串。这个过程的主要步骤包括:根据字符串、字体大小、格式等参数得到需要创建的位图大小,然后创建等大的、带Alpha通道的位图,最后将字符串绘制到该位图上。这里使用Alpha通道的原因是为了在OpenGL绘制时,利用OpenGL的Blend功能将位图中背景去掉。接下来,就该将这个位图绑定为纹理。
//*****************************************************************//
//得到位图的数据
Gdiplus::Rect rc(0,0, RectBound.Width, RectBound.Height);
bmp.LockBits(&rc,ImageLockModeRead,PF,pBitmapData);
bmp.UnlockBits(pBitmapData);
UCHAR * pData =(UCHAR *) pBitmapData->Scan0;
GLuint nTexID;
//绑定为纹理
glGenTextures(1,&nTexID);
glBindTexture(GL_TEXTURE_2D,nTexID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
//绑定为四通道的纹理
glTexImage2D(GL_TEXTURE_2D,0,4, RectBound.Width, RectBound.Height,0,GL_RGBA, GL_UNSIGNED_BYTE,pData);
//释放开辟的内存
if( pBitmapData!= NULL) delete pBitmapData;
return nTexID;
//*****************************************************************//
这样,我们就得到了一个RGBA格式四通道的纹理。这个函数在视图类的OnCreate函数中调用,进行纹理的初始化。最后,在OpenGL中以混合方式绘制这个纹理就实现了描边字体的绘制。在上面的函数中,最好再返回一个位图的大小,以便绘制的时候知道纹理的大小。下面是最后的效果图。
注:代码的工程文件为VS2008的C++工程,用VS2008打开。由于代码无法附在文后,所以放在资源里,可免费下载。