OpenGL -文字

本文详细介绍了如何在OpenGL中显示文本,从基本的ASCII字符到中文字符,再到利用纹理显示字体。涉及WindowsGDI函数、显示列表、宽字符、纹理字体以及缓冲区管理等技术。示例代码展示了动态生成字体纹理并进行文字滚动的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

OpenGL 没有自带专门的字库,因此,要显示文字,就必须依赖操作系统所提供的功
能

1. OpenGL 版 “hello world”

#include <windows.h>
// ASCII 字符 总共只有 0 - 127,共 128 种字符
#define  MAX_CHA 128

void drawString(const char* str)
{
	static int isFirstCall = 1;
	static GLuint lists;

	if(isFirstCall)
	{
		// 如果是第一次调用,执行初始化
		// 为每一个ASCII 字符产生一个显示列表
		isFirstCall = 0;
		// 申请 MAX_CHAR 个连续的显示列表编号
		lists = glGenLists(MAX_CHAR);

		// 把每个字符的绘制命令都装到对应的显示列表中
		wglUseFontBitmaps(wglGetCurrentDC(),0,MAX_CHAR,lists);		
	}
	// 调用每个字符对应的显示列表,绘制每个字符
	for(;*str != '\0';++str)
	{
		glCallList(lists + *str);
	}
}
/*
	显示列表一旦产生就一直存在,除非调用 glDeleteList 销毁。
	因此只需要在 第一次调用的时候初始化,以后就可以很方便的调用这些显
	示列表来绘制字符了
*/ 
void display(void)
{
	// 绘制字符的时候,可以先用 glColor*等指定颜色,然后用 glRasterPos*指定位置,最后调用显示列表来绘制。
	glClear(GL_COLOR_BUFFER_BIT);
	glColor3f(1.0f,0.0f,0.0f);
	glRasterPos2f(0.0f,0.0f);
	drawString("Hello,World");
	glutSwapBuffers();
}

指定字体

// 产生显示列表之前 Windows 允许选择字体
void selectFont(int size,int charset,const char* face)
{
	/*
		size : 字体大小
		charset :  字符集
		face : 字体名称
	*/
	HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
	charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
	DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face);
	HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
	DeleteObject(hOldFont);
}
void dispaly(void)
{
	selectFont(48,ANSI_CHARSET,"Comic Sans MS");
	glClear(GL_COLOR_BUFFER_BIT);
	glColor3f(1.0f,0.0f,0.0f);
	glRasterPos2f(0.0f,0.0f);
	drawString("Hello Word!");
	drawSwapBuffers();
}

// CreateFont 函数 为 Windows GDI 函数
// 如果需要在自己的程序中选择字体的话,把 selectFont 函数抄下来,在调用 glutCreateWindow 之后、在调用 wglUseFontBitmaps之前使用 selectFont 函数即可指定字体。

显示中文

通常我们在 C 语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字
符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用 
MultiByteToWideChar 函数,可以转化为所有的字符都占两个字节
// 转化的代码如下
// 计算字符的个数
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符
// 否则一个字节算一个字符
len = 0;
for(i = 0;str[i] != '\0';++i)
{
	if(IsDBCSLeadByte(str[i]))
	{
		++i;
	}
	++len;
}
// 将混合字符转化 为宽字符
wstring = (wchar_t*)malloc((len + 1) * sizeof(wchar_t));
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
wstring[len] = L'\0';

// 释放内存
free(wstring);
加上前面的 wglUseFontBitmaps 函数,可显示中文字符
void drawCNString(const char* str)
{
	int len,i;
	wchar_t* wstring;
	HDC hDC = wglGetCurrentDC();
	GLuint list glGenLists(1);

	/*
		 计算字符的个数
			如果是双字符的(比如中文),两个字节才算一个字符
			否则一个字节算一个字符
	*/	
	len = 0;
	for(i = 0;str[i] != '\0';++i)
	{
		if(IsDBCSLeadByte(str[i]))
		{
			++i;
		}
		++len;
	}
	// 将混合字符转化为宽字符
	wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
	wstring[len] = L'\0';

	// 逐个输出字符
	for(i=0; i<len; ++i)
	{
		wglUseFontBitmapsW(hDC, wstring[i], 1, list);
		glCallList(list);
	}

	// 回收所有临时资源
	free(wstring);
	glDeleteLists(list, 1);
}
/*
	用了 wglUseFontBitmapsW 函数,而不是 wglUseFontBitmaps。	
	wglUseFontBitmapsW 是 wglUseFontBitmaps 函数的宽字
	符版本,它认为字符都占两个字节。因为这里使用了 
	MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用
	wglUseFontBitmapsW
*/

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	selectFont(48, ANSI_CHARSET, "Comic Sans MS");
	glColor3f(1.0f, 0.0f, 0.0f);
	glRasterPos2f(-0.7f, 0.4f);
	drawString("Hello, World!");

	selectFont(48, GB2312_CHARSET, "楷体_GB2312");
	glColor3f(1.0f, 1.0f, 0.0f);
	glRasterPos2f(-0.7f, -0.1f);
	drawCNString("当代的中国汉字");

	selectFont(48, DEFAULT_CHARSET, "华文仿宋");
	glColor3f(0.0f, 1.0f, 0.0f);
	glRasterPos2f(-0.7f, -0.6f);
	drawCNString("傳統的中國漢字");

	glutSwapBuffers();
}

纹理字体

把文字放到纹理中可以任意修改字符的大小。

如何把文字放到纹理中?
	这里不是直接绘制到纹理,而是用简单的办法: 先把汉字绘制出来,成为像素,
	然后用 glCopyTexImage2D 把像素复制贷纹理。

	glCopyTexImage2D 与 glTexImage2D 的用法类似
		前者是直接把绘制好的像素复制贷纹理中,后者是从内存传送数据到纹理中。
// 先把文字绘制好
glRasterPos2f(XXX,XXX);
drawCNString("關");
// 分配纹理编号
glGenTextures(1,&texID);
// 指定为当前纹理
glBindTexture(GL_TEXTURE_2D,texID);
// 把像素作为纹理数据
// 将屏幕(0,0) 到 (64,64) 的矩形区域的像素复制到纹理中
glCopyTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,0,0,64,64,0);
// 设置纹理参数
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINIEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LIINEAR);
// 然后 就可以像使用普通的纹理一样,绘制各种物体时,指定合适的纹理坐标即可。
glRasterPos2f(XXX,XXX) r如何计算这个显示坐标。

计算文字宽度:
	Windows 专门提供了一个函数 GetCharABCWidths,它计算一系列连续字符的 
	ABC 宽度。所谓 ABC宽度,包括了 a, b, c 三个量,a 表示字符左边的空白宽度,
	b 表示字符实际的宽度,c 表示字符右边的空白宽度,三个宽度值相加得到整个字
	符所占宽度。

	如果只需要得到总的宽度,可以使用 GetCharWidth32 函数。如果要支持汉字,应
	该使用宽字符版本,即 GetCharABCWidthsW 和 GetCharWidth32W。在使用前需
	要用 MultiByteToWideChar 函数,将通常的字符串转化为宽字符串,就像前面的 
	wglUseFontBitmapsW 那样。

计算高度:
	指定字体的时候指定大小为 s 的话,所有的字符高度都为 s,只有宽度不同。
	如果我们使用 glRasterPos2i(-1, -1)从最左下角开始显示字符的话,其实是不能得
	到完整的字符的:。,因此我们应该设置字体的高度为 2 的整数次方,例如 16, 32, 
	64,这样用起来就会比较方便。
#define FONT_SIZE 64
#define TEXTURE_SIZE FONT_SIZE
/*
	首先要做的是将字符串转化为宽字符的形式,以便使用 
	wglUseFontBitmapsW 和 GetCharWidth32W函数。然后设置字体大小,接下
	来计算字体宽度,计算实际绘制的位置。然后产生显示列表,利用显示列
	表绘制字符,销毁显示列表。最后分配一个纹理编号,把字符像素复制到
	纹理中
*/
GLuint drawChar_To_Texture(const char* s)
{
	wchar_t w;
	HDC hDC = wglGetCurrentDC();

	/*
		选择字体字号、颜色
		不指定字体名字,操作系统提供默认字体
		设置颜色为白色
	*/
	selectFont(FONT_SIZE, DEFAULT_CHARSET, "");
	glColor3f(1.0f, 1.0f, 1.0f);

	// 转化为宽字符
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, s, 2, &w, 1);

	// 计算绘制的位置
	{
		int width, x, y;
		GetCharWidth32W(hDC, w, w, &width); // 取得字符的宽度
		x = (TEXTURE_SIZE - width) / 2;
		y = FONT_SIZE / 8;
		glWindowPos2iARB(x, y); // 一个扩展函数
	}

	// 绘制字符
	// 绘制前应该将各种可能影响字符颜色的效果关闭
	// 以保证能够绘制出白色的字符
	{
		GLuint list = glGenLists(1);
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_LIGHTING);
		glDisable(GL_FOG);
		glDisable(GL_TEXTURE_2D);
		wglUseFontBitmaps(hDC, w, 1, list);
		glCallList(list);
		glDeleteLists(list, 1);
	}
	// 复制字符像素到纹理
	// 注意纹理的格式
	// 不使用通常的 GL_RGBA,而使用 GL_LUMINANCE4
	// 因为字符本来只有一种颜色,使用 GL_RGBA 浪费了存储空间
	// GL_RGBA 可能占 16 位或者 32 位,而 GL_LUMINANCE4 只占 4 位
	{
	GLuint texID;
	glGenTextures(1, &texID);
	glBindTexture(GL_TEXTURE_2D, texID);
	glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4,0,0,TEXTURE_SIZE, TEXTURE_SIZE, 0);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	return texID;
}

纹理字体应用

1. 用多个四边形(实际上是矩形)连接起来,制作飘动的效果
2. 使用光照,计算法线向量
3. 把纹理融合进去
#define MIN_X (-0.5f)
#define MAX_X (0.5f)
#define MIN_Y (-0.5f)
#define MAX_Y (0.5f)
#define SEGS ((int)((MAX_X - MIN_X) * (512/2)))
#define RANGE (0.05f)
#define CIRCLES (2.0f)
#define SPEED (5.0f)
#define PI (3.1415926f)
/*
	SEGS 表示分段数,分段越多则显示越细致,这里定义成了与旗帜宽度相关的量。
	RANGE 表示摆动幅度
	CIRCLES 表示一面旗帜会出现多少个波峰,
	SPEED 表示摆动速度
	这三个量分别与公式 z = sin(t * c1 + c2) * c3 + c4 中的 c3, c2, c1 成正比。
	常量 PI,表示圆周率。	
*/
#include <math.h>
// theta 是一个随时间变化的量
GLfloat theta = 0.0f;

// 绘制一面旗帜
void draw(void)
{
	int i;
	// 每绘制一段,坐标 x 应该增加的量
	const GLfloat x_inc = (MAX_X - MIN_X) / SEGS;
	// 每绘制一段,纹理坐标 s 应该增加的量
	const GLfloat t_inc = 1.0f / SEGS;
	// 每绘制一段,常数 theta 应该增加的量
	const GLfloat theta_inc = 2 * PI * CIRCLES / SEGS;
	// 用 GL_QUAD_STRIP 来绘制相连的四边形
	glBegin(GL_QUAD_STRIP);
	
	for(i = 0;i <= SEGS;++i)
	{
		// 按照 z = sin(t * c1 + c2) * c3 + c4 的公式计算 z 坐标
		const GLfloat z = RANGE * sin(i*theta_inc + theta);
		// 一段只需要指定两个点
		// 第三个点其实是下一段的第一个点
		// 之所以使用三个点,是为了构成一个平面
		// 便于计算法线向量
		const GLfloat
			v1[] = {i*x_inc + MIN_X, MAX_Y, z},
			v2[] = {i*x_inc + MIN_X, MIN_Y, z},
			v3[] = {
				(i+1)*x_inc + MIN_X,
				MAX_Y,
				RANGE * sin((i+1)*theta_inc + theta)
			};
		// 调用一个函数来计算法线向量
		setNormal(v1, v2, v3);
		// 设置合适的纹理坐标和顶点坐标
		glTexCoord2f(i*t_inc, 1.0f);
		glVertex3fv(v1);
		glTexCoord2f(i*t_inc, 0.0f);
		glVertex3fv(v2);
	}
	glEnd();
}

// 系统空闲时调用
// 增加 theta 的值,然后重新绘制
void idle(void) 
{
	theta += (SPEED * PI / 180.0f);
	glutPostRedisplay();
}

//因为要使用光照,法线向量是不可少的。这里我们通过不共线的三个点来得到三个点所在平面的法线向量
/*
	设置法线向量
	三个不在同一直线上的点可以确定一个平面
	先计算这个平面的法线向量,然后指定到 OpenGL
*/
void setNormal(const GLfloat v1[3],const GLfloat v2[3],const GLfloat v3[3])
{
	// 首先根据三个点坐标,相减计算出两个向量
	const GLfloat s1[] = {
		v2[0]-v1[0], 
		v2[1]-v1[1], 
		v2[2]-v1[2]
	};
	const GLfloat s2[] = {
		v3[0]-v1[0], 
		v3[1]-v1[1], 
		v3[2]-v1[2]
	};
	// 两个向量叉乘得到法线向量的方向
	GLfloat n[] = {
		s1[1]*s2[2] - s1[2]*s2[1],
		s1[2]*s2[0] - s1[0]*s2[2],
		s1[0]*s2[1] - s1[1]*s2[0]
	};
	// 把法线向量缩放至单位长度
	GLfloat abs = sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]);
	n[0] /= abs;
	n[1] /= abs;
	n[2] /= abs;

	// 指定到 OpenGL
	glNormal3fv(n);
}

缓冲机制

改进缓冲机制性能,应该使用更高效的置换算法。
#include "ctbuf.h"
void display(void) 
{
	static int isFirstCall = 1;
	if( isFirstCall )
	{
		isFirstCall = 0;
		ctbuf_init(32, 256, "黑体");
	}
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_TEXTURE_2D);
	glPushMatrix();
	glTranslatef(-1.0f, 0.0f, 0.0f);
	ctbuf_drawString("美好明天就要到来", 0.1f, 0.15f);
	glTranslatef(0.0f, -0.15f, 0.0f);
	ctbuf_drawString("Best is yet to come", 0.1f, 0.15f);
	glPopMatrix();

	glutSwapBuffers();
}

const char* g_string ="《合金装备》(Metal Gear Solid)结尾曲歌词\n因为。。。。。。。。 \n美好即将到来\n"
textarea_t* p_textarea = NULL;

void display(void)
{
	static int isFirstCall = 1;

	if( isFirstCall )
	{
		isFirstCall = 0;
		ctbuf_init(24, 256, "隶书");
		p_textarea = ta_create(-0.7f, -0.5f, 0.7f, 0.5f,20, 10, g_string);
		glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
	}
	glClear(GL_COLOR_BUFFER_BIT);

	// 显示歌词文字
	glEnable(GL_TEXTURE_2D);
	ta_display(p_textarea);

	// 用半透明的效果显示一个方框
	// 这个框是实际需要显示的范围
	glEnable(GL_BLEND);
	glDisable(GL_TEXTURE_2D);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
	glRectf(-0.7f, -0.5f, 0.7f, 0.5f);
	glDisable(GL_BLEND);

	// 显示一些帮助信息
	glEnable(GL_TEXTURE_2D);
	glPushMatrix();
	glTranslatef(-1.0f, 0.9f, 0.0f);
	ctbuf_drawString("歌词显示程序", 0.1f, 0.1f);
	glTranslatef(0.0f, -0.1f, 0.0f);
	ctbuf_drawString("按 W/S 键实现上、下翻页", 0.1f, 0.1f);
	glTranslatef(0.0f, -0.1f, 0.0f);
	ctbuf_drawString("按 ESC 退出", 0.1f, 0.1f);
	glPopMatrix();

	glutSwapBuffers();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值