[置顶] stb_truetype解析ttf字体获取顶点信息

简介:

TTF(TrueTypeFont)是一种字库名称。TTF(TrueTypeFont)是Apple公司和Microsoft公司共同推出的字体文件格式,同时也是最常用的一种字体文件表示方式。TrueType采用几何学中二次B样条曲线及直线来描述字体的外形轮廓。所以我们可以很方便地把字体轮廓转换成曲线,可以对曲线进行填充,制成各种颜色和效果,它可以进一步变形,制作特殊效果字体。

本文将通过使用stb_truetype这个开源的库来对ttf文件进行解析,取得顶点信息,然后通过opengl分格化(libtess),将顶点信息转为三角面片,最后通过opengl对其进行统一渲染。

stb_truetype基本API:

注意:在包含stb_truetype.h头文件的时候需要定义STB_TRUETYPE_IMPLEMENTATION,否则将会无法使用。

1. 初始化字体信息

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对象中。

2. 取得竖直方向上的度量

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,即为行间距,用于设置多行间字体的间隔。

3. 取得水平方向间距

int advance = 0;
int lsb = 0;
stbtt_GetCodepointHMetrics(mFontInfo, code, &advance, &lsb);

通过传入当前的字体的unicode编码,则可以获取当前字体的水平宽度。

4. 取得字体的缩放比

float scale = stbtt_ScaleForPixelHeight(mFontInfo, pixels);

返回的即为字体根据当前大小返回的缩放比:计算公式为 scale = pixels / (ascent - descent);,pixels即为我们需要设置的字体的大小。

解析字体:

1. 创建一个opengl分格化的类:

Tessellation,具体可参考:Opengl分格化(libtess)移植与使用。

2. 通过字体的unicode取得当前字体的顶点信息:

stbtt_vertex* stbVertex = NULL;
int verCount = 0;
verCount = stbtt_GetCodepointShape(mFontInfo, code, &stbVertex);

函数返回顶点总数,顶点首地址保存在stbVertex指针对象中。

3. 顶点转换

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中。

4. 优化操作

由于解析出的顶点很多是重复的(绘制所需),所以我们将对顶点数据进行优化,只保存不重复的顶点,对于重复的顶点则用索引的方式来进行绘制。从而提升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来对其进行渲染:
虽然每个问题经过优化后顶点数得到了较大的降低,但是多个文字绘制时,加起来也是很庞大的。所以我们将通过VBO的方式来保存要渲染的顶点数据和所以数据,从而不需要每次渲染都将数据上传到opengl内部。

1. 创建VBO

glGenBuffers(VBO_MAX, mVbo);

由于我们需要两个缓冲区,所以创建一个数组大小为2的mVbo,用于保存缓冲区索引。

2. 取得每个字的顶点和索引

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来保存全部一次要绘制的文字的顶点和索引。同时通过计算字体的缩放和水平位置等来对每个字设定位置。

3. 绑定VBO

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);

如上所示,分别绑定顶点数据和索引数据。

4. 绘制

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的缓冲区中去取得数据,能够较大的提升渲染的性能。

你可能感兴趣的:(字体,font,OpenGL,truetype,stb_truetype)