本文将通过使用stb_truetype这个开源的库来对ttf文件进行解析,取得顶点信息,然后通过opengl分格化(libtess),将顶点信息转为三角面片,最后通过opengl对其进行统一渲染。
mFile = new FileRead("STXIHEI.TTF"); mFontInfo = new stbtt_fontinfo; stbtt_InitFont(mFontInfo, (unsigned char*)(mFile->getStream()), 0);
通过读取STXIHEI.TTF文件,取得当前文件的数据流,创建一个stbtt_fontinfo的对象,同于保存stbtt信息。接着调用stbtt_InitFont,来对文件进行解析,最后将信息保存在mFontInfo对象中。
int ascent = 0; int descent = 0; int lineGap = 0; stbtt_GetFontVMetrics(mFontInfo, &ascent, &descent, &lineGap); mLineHeight = ascent - descent + lineGap;
ascent即字体从基线到顶部的高度;descent即基线到底部的高度,正常为负值;lineGap即为行间距,两个字体之间的间距。所以真正两个字体之间的距离为 ascent - descent + lineGap,即为行间距,用于设置多行间字体的间隔。
int advance = 0; int lsb = 0; stbtt_GetCodepointHMetrics(mFontInfo, code, &advance, &lsb);
通过传入当前的字体的unicode编码,则可以获取当前字体的水平宽度。
float scale = stbtt_ScaleForPixelHeight(mFontInfo, pixels);
返回的即为字体根据当前大小返回的缩放比:计算公式为 scale = pixels / (ascent - descent);,pixels即为我们需要设置的字体的大小。
Tessellation,具体可参考:Opengl分格化(libtess)移植与使用。
stbtt_vertex* stbVertex = NULL; int verCount = 0; verCount = stbtt_GetCodepointShape(mFontInfo, code, &stbVertex);
函数返回顶点总数,顶点首地址保存在stbVertex指针对象中。
void TrueType::_convert(void* stbVertex, int verCount) { Vertex current; Vertex previous; mTess->beginPolygon(); mTess->beginContour(); for (int nIdx = 0; nIdx<verCount; nIdx++) { stbtt_vertex* stbVer = ((stbtt_vertex*)stbVertex+nIdx); switch (stbVer->type) { case STBTT_vmove: current.x = stbVer->x; current.y = stbVer->y; mTess->endContour(); mTess->beginContour(); break; case STBTT_vline: mTess->insertVertex(current); current.x = stbVer->x; current.y = stbVer->y; break; case STBTT_vcurve: { mTess->insertVertex(current); Vertex contrl; contrl.x = stbVer->cx; contrl.y = stbVer->cy; previous = current; current.x = stbVer->x; current.y = stbVer->y; //insert the middle point mTess->insertVertex(_getBezierPoint(previous, contrl, current, 0.5)); } break; default:break; } } mTess->endContour(); mTess->endPolygon(); }
如代码所示,分别对每个顶点进行遍历,ttf字体都是以直线或者二次贝塞尔曲线进行存储。主要有三种状态:
STBTT_vmove 移动,即当前不绘制,将当前的点移动到指定的位置。所以我们只要保持当前点即可,同时要结束上一次轮廓线的指定mTess->endContour();,同时开始下一次的轮廓线指定。
STBTT_vline 即绘制直线,即将上一次的顶点插入到轮廓线insertVertex,将当前的顶点保持。
STBTT_vcurve 即当前是绘制二次贝塞尔曲线。首先插入上次的顶点,接着取得当前贝塞尔曲线的控制点和顶点,此时有三个顶点,一个是上一次的顶点p0,一个是控制点p1,一个是当前的顶点p2。则通过二次贝塞尔曲线方程,则可计算曲线上的点。
y = (1-t)*(1-t)p0 + 2*t*(1-t)p1 + t*t p2;
对于精度要求比较高的,可以循环取得曲线上的点,我们这边则直接取得中点(t=0.5)。最后将取得的点插入到轮廓线中。
当全部的顶点都已经遍历结束后,调用mTess->endPolygon();,此时则将由opengl分割器进行分割。我们通过配置libtess来限制只将当前的信息分格为三角面片(GL_TRIANGLES),便于后面的绘制。最后将分格好的三角面片信息保存在Tessellation的mTessVertex中。
由于解析出的顶点很多是重复的(绘制所需),所以我们将对顶点数据进行优化,只保存不重复的顶点,对于重复的顶点则用索引的方式来进行绘制。从而提升opengl绘制的性能。
std::vector<Vertex>* tessVertex = mTess->getVertex(); std::vector<Vertex>::iterator iter = tessVertex->begin(); std::vector<Vertex>::iterator iterfind; fontInfo->mIndicesSize = tessVertex->size(); fontInfo->mIndices = new GLuint[tessVertex->size()]; GLuint* indPoint = fontInfo->mIndices; for (iter; iter != tessVertex->end(); iter++) { int i = _findTheIndex(fontInfo->mVertexList, *iter); if (-1 != i) { *indPoint++ = i; } else { *indPoint++ = fontInfo->mVertexList.size(); fontInfo->mVertexList.push_back(*iter); } }如上所示,创建一个FontInfo的结构体指针,用于保存字体信息:
struct FontInfo { int mCode; //字体编码 std::vector<Vertex> mVertexList; //顶点列表 GLuint* mIndices; //索引数组 GLuint mIndicesSize;//索引数组大小 };
通过上述方式,保存了当前的字的顶点数据和索引数据,最后保存在结构体指针fontInfo中。对于上述的优化至少能减少50%的顶点数,所以对于后续的渲染性能的提升还是很可观的,同时对于移动设备等,能够极大的减少系统的带宽和功耗。
通过上面的方式,取得某一个字的顶点信息与索引信息后,接下来将通过opengl来对其进行渲染:
虽然每个问题经过优化后顶点数得到了较大的降低,但是多个文字绘制时,加起来也是很庞大的。所以我们将通过VBO的方式来保存要渲染的顶点数据和所以数据,从而不需要每次渲染都将数据上传到opengl内部。
glGenBuffers(VBO_MAX, mVbo);
由于我们需要两个缓冲区,所以创建一个数组大小为2的mVbo,用于保存缓冲区索引。
std::vector<Vertex> vertexList; std::vector<GLuint> indicesList; int beginPos = 0; int preCode = 0; int fontDis = 0; int fontHeight = 0; if (mFontInfoList.empty()) { return ; } float nScale = mTrueType->getScaleForPixelHeight(mSize); for (int fontIdx=0; fontIdx<mFontInfoList.size(); fontIdx++) { if (mFontInfoList[fontIdx]->mCode == '\n') { fontHeight -= (mTrueType->getLineHeight()+mLineDis*mLineDis) * nScale; fontDis = 0; continue; } std::vector<Vertex>& vertex = mFontInfoList[fontIdx]->mVertexList; int verCount = vertex.size(); beginPos = vertexList.size(); //every font vertex Vertex v; for (int i=0; i<verCount; i++) { v.x = vertex[i].x * nScale + fontDis + mPos.x; v.y = vertex[i].y * nScale + fontHeight + mPos.y; vertexList.push_back(v); } //indices for (int i=0; i < mFontInfoList[fontIdx]->mIndicesSize; i++) { indicesList.push_back(*(mFontInfoList[fontIdx]->mIndices+i) + beginPos); } //font dis int fontWidth = mTrueType->getAdvance(mFontInfoList[fontIdx]->mCode); int kernDis = mTrueType->getKernAdvance(preCode, mFontInfoList[fontIdx]->mCode); fontDis += (fontWidth + fontDis + mDis*mDis)*nScale; preCode = mFontInfoList[fontIdx]->mCode; }
如上代码所示,通过vertexList和indicesList来保存全部一次要绘制的文字的顶点和索引。同时通过计算字体的缩放和水平位置等来对每个字设定位置。
glBindBuffer(GL_ARRAY_BUFFER, mVbo[0]); glBufferData(GL_ARRAY_BUFFER, vertexList.size()*sizeof(Vertex), &vertexList[0], GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mVbo[1]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesList.size()*sizeof(GLuint), &indicesList[0], GL_STATIC_DRAW);
如上所示,分别绑定顶点数据和索引数据。
GLuint vertPos = shader->getId("vertPosition", Type_Attribute); glBindBuffer(GL_ARRAY_BUFFER, mVbo[0]); glVertexAttribPointer(vertPos, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(vertPos); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mVbo[1]); glDrawElements(GL_TRIANGLES, mIndicesCount, GL_UNSIGNED_INT, 0);
取得着色器中的顶点的索引,同时绑定vbo来设置顶点到着色器中,如上所示,只需要绑定vbo和设置顶点的组合方式即可,不需要再上传顶点数据(最后一个参数为0即可说明)。最后在绘制的时候通过调用glDrawElements来进行全部顶点数据的绘制。第一个参数为GL_TRIANGLES,说明我们的顶点都是以单独的三角形来绘制的。同时这边的第二个参数需要设置总的需要绘制的顶点数,即可完成绘制。
通过使用VBO,数据只需要在第一次的时候上传到opengl中的缓冲区中,接下来的绘制都不需要再次上传,只需要从opengl的缓冲区中去取得数据,能够较大的提升渲染的性能。