opengl es2.0 渲染文字

在OpenGL 家族中是没有提供直接渲染文字的接口,所以我们要在opengl中显示文字,就需要借助于其他的三方库或者自己解析绘制文字,然后使用opengl中的绘制接口去渲染出文字

这里我使用的freetype这个三方库·

FreeType 2.0

     FreeType是一个完全开源的、可扩展、可定制且可移植的字体引擎,它提供TrueType字体驱动的实现统一的接口来访问多种字体格式文件,

包括点阵字、TrueType、OpenType、Type1等

(1)它使得客户应用程序可以方便地访问字体文件,无论字体文件存储在哪里,并且与字体格式无关。
(2)能方便地提取全局字体数据,这些数据普遍存在于一般的字体格式中。(例如:全局度量标准,字符编码/字符映射表,等等)
(3)能方便地提取某个字符的字形数据(度量标准,图像,名字等其他任何数据)
(4)具备访问字体格式特定的功能(例如:SFNT表,多重控制,OpenType轮廓表)

关于freetype的使用方法,网上有很多技术文档这里就不再详述,有兴趣的朋友可以参考以下几个链接

http://blog.csdn.net/vrix/article/details/2873436

http://www.unixresources.net/linux/clf/kylix/archive/00/00/59/21/592188.html

http://blog.csdn.net/finewind/article/details/38009731

纹理偏移图(Texture Atlas)

     将许多小的纹理合并拼接到一张大图当中计算好对应的纹理坐标,然后在需要用到的时候,加载这张大图然后使用对应的纹理坐标显示出想要的效果。

这样做的好处就是,就是合并纹理后,在渲染的时候可以减少opengl切换纹理的频率, opengl在绑定纹理也就是调用glBindTexture 效率是比较低的。

文字渲染的方式

     在opengl中显示文字,有如下几种方式:

          a)一个字生成一张位图,然后贴图

          b)一串字生成一张位图,然后贴图

          c)先将需要用到的字生成到一张位图中,然后需要渲染的时候根据每个字所在位图的纹理坐标然后贴图显示

     下面我们来分析一下各种方式的优缺点:

          a这种方式是灵活性最高的一种,可以任意指定单个字的大小啊颜色啊描边什么的,但是如果视口中需要显示的文字比较多,这种方式所带来opengl纹理切换

      频率会极大的拖慢opengl的渲染效率

          b这种方式可以任意指定一串字的样式,但是对于单个字样式的指定就比较欠缺,并且相对于a在渲染效率上面会有很大的提升

          c这种方式的文字渲染效率相比于a、b,能够极大的提升字体渲染效率,但是内存占用上会比较多,也多了很多限制(之后详述)

     上述的三种方式都有自己的优缺点,至于具体应该选用哪种方式,还是得根据需求来选取最优的方式

  下面开始就要介绍主要内容了,使用文字偏移图方式渲染文字,主要分三部分进行:生成文字偏移图、解析绘制文本、渲染文本

 生成文字偏移图

     要生成文字偏移图就要用到freetype库和我上面介绍的纹理偏移图技术,基本流程可以概括为:从freetype中根据字体取出所有字符的字模,然后逐个拼接到

 一张大的纹理当中,然后生成一个字符映射表(主要是字符到纹理坐标)

     整个流程看起来好像很简单,其实并不然,对于unicode来说,宽字节范围0~65535,然后opengl对于一张位图的大小也是有限制的,可以通过如下方式获取到

  1. GLint max;  
  2. glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);  


, 假设最大的尺寸是1024x1024的一个字符按照16x16的大小来算一张位图最多也只能放置

4096个字符,如果想要缓存全部字符,至少也是10+张偏移图了,这样显然是不合理的,毕竟我们平时并不是所有的字符都会用到,大部分的字符我们是用不到的,我们

只需要能存进我们用到字符到偏移图里就可以了,但是很遗憾,字符编码里并没有划定固定的哪段是常用的而哪些是不常用的,这里我的处理方式是从网上收集到其他

人整理到的3500个字符先到纹理偏移图中,然后如果之后有新的字符产生,就再加进去(会影响到一定的效率)。

   其实对于常用的应用程序来说4096个字符基本足够使用了,但是如果需要缓存的字符超过了4096了呢?  显然一张字符图肯定是不够使用的,在这里我在生产偏移图

的地方加入了当超过了一张图的字符缓存范围后,再创建一张字符偏移图,然后再字符映射表里添加了关于字符偏移图的索引(既一个字符具体在那张偏移图里)

简要代码参考如下:

  1. {//从freetype库中取出ch字符绘制到纹理中  
  2.         int textureSize = MAXTEXTURESIZE;  
  3.         u32 idx = FT_Get_Char_Index(mFace, ch);//ch就是字符unicode  
  4.         if(idx <= 0)  
  5.         {  
  6.             return;  
  7.         }  
  8.         u32 top  = 0;  
  9.         u32 left = 0;  
  10.         u32 texw = 0;  
  11.         u32 texh = 0;  
  12.   
  13.         u32 ftsize = SINGLEFONTSIZE ;  
  14.         if(!FT_Load_Glyph(mFace, idx, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP))  
  15.         {  
  16.             FT_GlyphSlot glyph = (mFace)->glyph;  
  17.             FT_Bitmap bits;  
  18.             if( ft_glyph_format_outline == glyph->format)  
  19.             {  
  20.                 if(!FT_Render_Glyph(glyph, FT_RENDER_MODE_NORMAL))  
  21.                 {//取字模  
  22.                     bits = glyph->bitmap;  
  23.                     u8 *pt = bits.buffer;  
  24.                     top  = glyph->bitmap_top;  
  25.                     left = glyph->bitmap_left;  
  26.   
  27.                     texw = bits.width;  
  28.                     texh = bits.rows;  
  29.                     u32 Ascender = (u32)(mFace)->size->metrics.ascender >> 6;  
  30.                     u32 charx = left  ;  
  31.                     u32 chary = Ascender - top - 3;  
  32.                       
  33.                     if( mCurY + ftsize > textureSize )  
  34.                     {//如果有单张字体偏移图绘制满则创建新的偏移图并设置好索引  
  35.                         createNewAtlasMap();  
  36.                     }  
  37.                     //创建一个16x16的纹理数据流(a8r8g8b8的格式,)  
  38.                     video::IImage *img =  
  39.                     mp::base::IMpSite::getSite()->getImageFactory()->create(video::ECF_A8R8G8B8, Dimension2du(ftsize,ftsize));  
  40.                     u32 *texp = (u32*)img->lock();  
  41.                     memset(texp, 0, ftsize * ftsize * sizeof(u32));  
  42.                     for(int i = 0;i < bits.rows;++i)  
  43.                     {//根据字模数据写对应的数据流  
  44.                         u32 *rowp = texp;  
  45.                         for(int j = 0;j < bits.width;++j)  
  46.                         {  
  47.                             if( 0 != *pt)  
  48.                             {  
  49.                                 //字体颜色加深 BEGIN  
  50.                                 u8 tmp = *pt;  
  51.                                 *pt = (u8)(sqrt((float)tmp / 256.0f) * 256.0f);  
  52.                                 //字体颜色加深 END  
  53.                                 *rowp = *pt << 24;//A值  
  54.                                 *rowp |= 0xffffff;//白色文字  
  55.                             }  
  56.                             else  
  57.                             {  
  58.                                 *rowp = 0;  
  59.                             }  
  60.                             pt++;  
  61.                             rowp++;  
  62.                         }  
  63.                         texp += ftsize;  
  64.                     }  
  65.                     //设置字符单元信息  
  66.                     CharPos *wch = new CharPos;  
  67.                     wch->left   = (mCurX )/ (float)textureSize;  
  68.                     wch->top    = (mCurY )/ (float)textureSize;  
  69.                     wch->right  = (mCurX + ftsize) / (float)textureSize;  
  70.                     wch->bottom = (mCurY + ftsize) / (float)textureSize;  
  71.                     wch->width  = texw;  
  72.                     wch->wspace = charx;  
  73.                     wch->index = mIndex;  
  74.                     mCharMap.insert(std::make_pair(ch, wch));  
  75.                     if(NULL != mFontAtlasTexture[mIndex] && NULL != mFontAtlasTexture[mIndex]->getImage())  
  76.                     {//拼接纹理,按照a8r8g8b8的格式流,将小的纹理流数据写到对应的位置  
  77.                         img->copyTo(mFontAtlasTexture[mIndex]->getImage(),core::position2d<int>(mCurX,mCurY + chary));  
  78.                         img->drop();  
  79.                     }  
  80.                     mCurX += ftsize + 1;  
  81.                     if( mCurX + ftsize > textureSize )  
  82.                     {//当一排写满则进入下一排  
  83.                         mCurX = 0;  
  84.                         mCurY += ftsize ;  
  85.                     }  
  86.                 }  
  87.             }  
  88.         }  
  89.     }  

上述代码片段,主要就是从freetype中取出字模然后根据字模写好对应的image数据,然后将image数据写到对应大的image的数据中(大纹理)

这部分内容我在windows上有一个简单的小demo在git上 地址:https://github.com/tlglovewf/MyGitHub  以下图就是我生成文字偏移图(微软雅黑)

 

解析绘制文本

    上面进行了第一步,生成好了文字偏移图,也生成好了字符映射表,下面就是要根据输入的文本绘制出对应的文字,先进行解析

 这个就很简单了,字符都是按照unicode来解析的(freetype默认的就是unicode)

  1. //颜色分组  
  2.      typedef std::map<FontColor, irr::core::array<mp::draw::SDrawVertexUV1> > FontColorMap;  
  3.      //偏移图分组  
  4.      typedef std::map<AtlasMapIndex ,FontColorMap > FontAtlasMap;  
  5.      FontAtlasMap                      mfAtlasMap;  

 

  1. CharPos* CMpFontBuilder::getCharacter(WCHAR inWCh)  
  2. {  
  3.     if(mCharMap.end() == mCharMap.find(inWCh))  
  4.     {//若现有的映射表中没有输入字符则添加新字到偏移图中  
  5.         drawSingleCharToTexture(inWCh);  
  6.         mbIsUpdated = true;  
  7.     }  
  8.   
  9.     return mCharMap[inWCh];  
  10. }  

 

  1. if( NULL != mpDrawProgram )  
  2.       {  
  3.           float halfFontSize = fontSize * 0.5 ;  
  4.           float rate = fontSize / (float)SINGLEFONTSIZE;  
  5.           int charWidth = 0;  
  6.           Vector3df textPos = Vector3df(inPos.X + halfFontSize,inPos.Y + halfFontSize,0.0) ;  
  7.           const wchar_t *text = str.c_str();  
  8.           int charHeight = 0;  
  9.           while(*text)  
  10.           {  
  11.               WCHAR ch = *text++;  
  12.               if(ch == '\n')  
  13.               {  
  14.                   charHeight += fontSize;  
  15.                   charWidth = 0;  
  16.                   continue;  
  17.               }  
  18.               CharPos *pos = mpFontBuilder->getCharacter(ch);  
  19.               if( NULL != pos )  
  20.               {  
  21.                   mp::draw::SDrawVertexUV1 ftpos;  
  22.                   ftpos.pos = textPos + Vector3df( charWidth - halfFontSize, charHeight + halfFontSize,0.0 );  
  23.                   ftpos.uv1 = Vector2df( pos->left, pos->bottom );  
  24.                   mfAtlasMap[pos->index][fontColor].push_back(ftpos);  
  25.                   ftpos.pos = textPos +Vector3df( charWidth + halfFontSize, charHeight + halfFontSize,0.0 );  
  26.                   ftpos.uv1 = Vector2df( pos->right, pos->bottom );  
  27.                   mfAtlasMap[pos->index][fontColor].push_back(ftpos);  
  28.                   ftpos.pos = textPos + Vector3df( charWidth + halfFontSize, charHeight - halfFontSize,0.0 );  
  29.                   ftpos.uv1 = Vector2df( pos->right, pos->top );  
  30.                   mfAtlasMap[pos->index][fontColor].push_back(ftpos);  
  31.                   ftpos.pos = textPos + Vector3df( charWidth - halfFontSize, charHeight - halfFontSize,0.0 );  
  32.                   ftpos.uv1 = Vector2df( pos->left, pos->top );  
  33.                   mfAtlasMap[pos->index][fontColor].push_back(ftpos);  
  34.                   if(ch < 127)  
  35.                   {//主要用于英文、标点 字符间隙的调整  
  36.                       charWidth += (pos->width - pos->wspace + 2) * rate;  
  37.                   }  
  38.                   else  
  39.                   {//保持其他字符间隔(主要是中字)  
  40.                       charWidth += ( fontSize - pos->wspace) ;  
  41.                   }  
  42.               }  
  43.           }  
  44.       }  

 

这里先按照偏移图索引和字体颜色对单个字符进行了分组(这里是由于项目中字体颜色不多可以这么处理),为什么这里要分组,这里就要讲一下使用字体偏移图的一些缺陷,

 

首先是在缓存字符的使用单个字符是16x16这样就会导致如果需要绘制的字符字号较大就会出现失真,解决方案就是要再生成张字符比较大的字符图(改变字体也是)

然后就是颜色,如果需要显示不同颜色的字符,要么就是在shader里面做调整要么就是再生成一张需要颜色的字符偏移图.  我先按照字符所在的字符图索引分一个组(为了减少纹理切换频率)然后再按颜色分组(在shader中处理过显示颜色,按照不同颜色分批次渲染即可)

渲染文本

 上面一步只是先将所有要显示的字进行分组,然后最后将这些分好组的字符,尽可能用最少的方式将其渲染出来

  1. mpDrawProgram->activate();  
  2.            mpDrawProgram->updateDrawState(mpDrawState);       
  3.            FontAtlasMap::iterator it = mfAtlasMap.begin();  
  4.            FontAtlasMap::iterator ed = mfAtlasMap.end();  
  5.            while( it != ed )  
  6.            {//根据字体所在偏移图的index进行分组  
  7.                mp::draw::IDrawTexture *pTexture = mpFontBuilder->getFontAtlasTexture(it->first);  
  8.                mat.setTexture(0, pTexture);  
  9.                FontColorMap::iterator clIt = it->second.begin();  
  10.                FontColorMap::iterator clEd = it->second.end();  
  11.                while(clIt != clEd)  
  12.                {//然后根据不同颜色的字再进行分批次渲染  
  13.                    mat.DiffuseColor = clIt->first;  
  14.                    irr::core::array<mp::draw::SDrawVertexUV1> &array = clIt++->second;  
  15.                    irr::core::array<u16> indices;  
  16.                    int len  = array.size() / 4;  
  17.                    for(int i = 0; i < len; ++i )  
  18.                    {//根据分组字符添加索引  
  19.                        int dt = i << 2;  
  20.                        indices.push_back(0 + dt);  
  21.                        indices.push_back(1 + dt);  
  22.                        indices.push_back(2 + dt);  
  23.                        indices.push_back(0 + dt);  
  24.                        indices.push_back(2 + dt);  
  25.                        indices.push_back(3 + dt);  
  26.                    }  
  27.                    //shader中处理是否加载到显存后删除对象  
  28.                    mat.MaterialTypeParam2 = false;  
  29.                    mpDrawProgram->setMaterial(mat);  
  30.                    mpDrawProgram->drawVertexPrimitiveList(array.pointer(), 1, indices.pointer(), indices.size(), sizeof(mp::draw::SDrawVertexUV1), mp::draw::EDVF_POS_UV1, mp::draw::EDIF_U16, mp::draw::EDPT_TRIANGLES, true);  
  31.                }  
  32.                ++it;  
  33.            }  
  34.            mpDrawProgram->deactivate();  

上述代码主要做的就是先根据偏移图index分组好的字符分一个批次渲染,然后在一个字符偏移图里再根据颜色分一个批次渲染(这里由于使用的颜色不多,所以这么处理对于效率影响不大,如果对色彩要求比较多,就要再考虑别的方案了)

总结

    以上内容肯有些地方写的比较简陋,但是大体流程就是这样,想要提升opengl渲染效率,就要尽可能的降低纹理切换频率,减少渲染批次(opengl es2.0 一个批次就是调用一次glDrawArray/glDrawElements), 使用字体贴图渲染文本牺牲了一部分内存去存储字体偏移图,以及降低了文字渲染的灵活性(看需求),但是可以极大的降低纹理切换的频率(一般情况下所有文字都能在一张图里,也只用切换一次即可),减少渲染批次(如果没有别的颜色或者字号需求,所有文字的渲染批次也就一次),极大的提升了渲染效率

    使用文字偏移图来绘制文字,比较适用于对文字多样式要求不高的应用(如果样式太多所需要创建的纹理偏移图就比较多,或者要对shader进行调整),如果对文字样式有很高的要求就要考虑前面的a/b方法,并好好研习研习freetype库

 由于本人技术的限制,上述内容会有很多不足,待日后逐步完善,如有更好的方法也欢迎来指正,谢谢!

这篇文章是我写在公司项目组博客下的 源地址:http://blog.csdn.net/fastmapmobileteam/article/details/50237935

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值