轮廓字体(Outline Fonts)
这节课继续上一节课课的内容。在第13课我们学习了如何使用位图字体,这节课,我们将学习如何使用轮廓字体。
创建轮廓字体的方法类似于在第13课中我们创建位图字体的方法。但是,轮廓字体看起来要酷100倍!你可以指定轮廓字体的大小。轮廓字体可以在屏幕中以3D方式运动,而且轮廓字体还可以有一定的厚度!而不是平面的2D字符。使用轮廓字体,你可以将你的计算机中的任何字体转换为OpenGL中的3D字体,加上合适的法线,在有光照的时候,字符就会被很好的照亮了。
一个小注释,这段代码是专门针对Windows写的,它使用了Windows的wgl函数来创建字体,显然,Apple机系统有agl,X系统有glx来支持做同样事情的,不幸的是,我不能保证这些代码也是容易使用的。如果哪位有能在屏幕上显示文字且独立于平台的代码,请告诉我,我将重写一个有关字体的教程。
我们从第一课的典型代码开始,添加上stdio.h头文件以便进行标准输入/输出操作,另外,stdarg.h头文件用来解析文字以及把变量转换为文字。最后加上math.h头文件,这样我们就可以使用SIN和COS函数在屏幕中移动文字了。
#include <windows.h> // Header File For Windows
#include <math.h> // Header File For Windows Math Library
#include <stdio.h> // Header File For Standard Input/Output
#include <stdarg.h> // Header File For Variable Argument Routines
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The Glaux Library
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // Holds The Instance Of The Application 另外,我们还要添加2个变量。base将保存我们创建的第一个显示列表的编号。每个字符都需要有自己的显示列表。例如,字符‘A’在显示列表中是65,‘B’是66,‘C’是67,等等。所以,字符‘A’应保存在显示列表中的base + 65这个位置。
我们再添加一个叫做rot的变量。用它配合SIN和COS函数在屏幕上旋转文字。我们同时用它来改变文字的颜色。
GLuint base; // Base Display List For The Font Set
GLfloat rot; // Used To Rotate The Text
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default GLYPHMETRICSFLOAT gmf[256]用来保存256个轮廓字体显示列表中对应的每一个列表的位置和方向的信息。我们通过gmf[num]来选择字母。num就是我们想要了解的显示列表的编号。在稍后的代码中,我将说明如何如何检查每个字符的宽度,以便自动将文字定位在屏幕中心。切记,每个字符的宽度可以不相同。Glyphmetrics会大大简化我们的工作。
GLYPHMETRICSFLOAT gmf[256];// Storage For Information About Our Outline Font Characters
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);//Declaration For WndProc下面这段用来构建真正的字体的代码类似于我们创建位图字体的方法。和13课一样,这段代码是最难解释的部分。
‘HFONT font’用来保存我们使用的Windows字体的ID。
接下来我们定义base。我们使用glGenLists(256)创建一组共256个显示列表。在显示列表创建好以后,变量base将保存第一个显示列表的编号。
GLvoid BuildFont(GLvoid) // Build Our Bitmap Font
{
HFONT font; // Windows Font ID
base = glGenLists(256); // Storage For 256 Characters还有更多有趣的部分,我们将创建属于自己的轮廓字体。我们从指定字体的大小开始,你会注意到它是一个负数,我们通过加上一个负号来告诉Windows寻找一个基于CHARACTER高度的字体。如果我们使用一个正数,就是寻找一个与基于CELL的高度相匹配的字体。
font = CreateFont( -12, // Height Of Font然后我们指定每个单元的宽度,你会注意到我把它定义为0,这样,Windows就会使用默认值。如果你愿意的话,可以改变它的值,比如更宽一点,等等。
0, // Width Of FontAngle Of Escapement会将字体旋转。MSDN帮助中解释Orientation Angle用于指定每个字的底边和显示设备的X轴之间的角度,每个单位是十分之一个角度,不幸的是我对这个没有概念。
0, // Angle Of Escapement
0, // Orientation Angle字体重量是一个很重要的参数,你可以设置一个0-1000之间的值或使用一个已定义的值。FW_DONTCARE是0, FW_NORMAL是400, FW_BOLD是700 and FW_BLACK是900。还有许多预先定义的值,但是这四个的效果比较好。值越大,字体就越粗。
FW_BOLD, // Font WeightItalic(斜体),Underline(下划线)和Strikeout(删除线)可以是TRUE或FALSE。如果将Underline设置为TRUE,那么字体就会带有下划线,否则就没有,非常简单。
FALSE, // Italic
FALSE, // Underline
FALSE, // StrikeoutCharacter Set Identifier(字符集标识符)用来描述你要使用的字符集(内码)类型。有太多需要说明的类型了。CHINESEBIG5_CHARSET,GREEK_CHARSET,RUSSIAN_CHARSET,DEFAULT_CHARSET ,等等。我使用的是ANSI,尽管DEFAULT也是很好用的。
如果你有兴趣使用Webdings或Wingdings等字体,你必须使用SYMBOL_CHARSET而不是ANSI_CHARSET。
ANSI_CHARSET, // Character Set IdentifierOutput Precision(输出精度)非常重要。它告诉Windows在有多种字符集的情况下使用哪类字符集。OUT_TT_PRECIS告诉Windows如果一个名字对应多种不同的选择字体,那么选择字体的TRUETYPE类型。Truetype字体通常看起来要好些,尤其是你把它们放大的时候。你也可以使用OUT_TT_ONLY_PRECIS,它将会一直尝试使用一种TRUETYPE类型的字体
OUT_TT_PRECIS, // Output Precision裁剪精度是一种当字体落在裁剪范围之外时使用的剪辑类型,不用多说,只要把它设置为DEFAULT就可以了。
CLIP_DEFAULT_PRECIS, // Clipping Precision输出质量非常重要。你可以使用PROOF,DRAFT,NONANTIALIASED,DEFAULT或ANTIALISED。
我们都知道,ANTIALIASED字体看起来很好,将一种字体Antialiasing(反锯齿)可以实现在Windows下打开字体平滑时同样的效果,它使任何东西看起来都要少些锯齿,也就是更平滑。
ANTIALIASED_QUALITY, // Output Quality下面是Family和Pitch设置。Pitch属性有DEFAULT_PITCH,FIXED_PITCH和VARIABLE_PITCH,Family有FF_DECORATIVE,FF_MODERN,FF_ROMAN,FF_SCRIPT,FF_SWISS,FF_DONTCARE.尝试一下这些值,你就会知道它们到底有什么功能。我把它们都设置为默认值。
FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch最后,是我们要用的字体的确切的名字。打开Microsoft Word或其它什么文字处理软件,点击字体下拉菜单,找一个你喜欢的字体。将‘Comic Sans MS’替换为你想用的字体的名字,你就可以使用它了。(中文还不行,需要别的方法)
"Comic Sans MS"); // Font Name现在,将刚才创建的字体选入我们的DC。
SelectObject(hDC, font); // Selects The Font We Want
}下面是新的代码。我们使用一个新的命令wglUseFontOutlines来创建轮廓字体。我们选择我们的DC,首字符,将要创建的字符的个数和‘base’显示列表值。所有一切都与我们创建位图字体的方法类似。
wglUseFontOutlines( hDC, // Select The Current DC
0, // Starting Character
255, // Number Of Display Lists To Build
base, // Starting Display Lists但是,这还不是全部。接下来我们设置偏差等级。这个值越接近0.0f, 字体看起来就越平滑。设置完偏差值后,我们设置字体的厚度。它用来描述字体在Z轴上的厚度。0.0f会产生一个平面的2D字体,1.0f会产生一个有一定厚度的字体。
参数WGL_FONT_POLYGONS告诉OpenGL使用多边形来创建一个实心字体。如果我们使用WGL_FONT_LINES的话,字体就变成了轮廓线(由线组成),值得注意的是,如果使用GL_FONT_LINES,就不能设置法线,光照也就不能正常工作。
最后一个参数gmf指向显示列表数据的地址缓存。
0.0f, // Deviation From The True Outlines
0.2f, // Font Thickness In The Z Direction
WGL_FONT_POLYGONS, // Use Polygons, Not Lines
gmf); // Address Of Buffer To Recieve Data接下来的代码很简单。它在内存中从base开始删除256个显示列表。我不知道Windows是否会做这些工作,但还是保险为好。
GLvoid KillFont(GLvoid) // Delete The Font List
{
glDeleteLists(base, 256); // Delete All 256 Characters
}
下面就是我优异的GL文字程序了。你可以通过调用glPrint(“需要写的文字”)来调用这段代码。与第13课中绘制位图字体的方法完全相同,文字被存储在字符串 * fmt中。
GLvoid glPrint(const char *fmt, ...) // Custom GL "Print" Routine
{
下面的第一行定义了一个叫做length的变量。我们使用这个变量来查询字符串的长度。第二行创建了一个大小为256个字符的字符数组,里面保存我们想要的文字串。第三行创建了一个指向一个变量列表的指针,我们在传递字符串的同时也传递了这个变量列表。如果我们传递文字时也传递了变量,这个指针将指向它们。
float length = 0; // Used To Find The Length Of The Text
char text[256]; // Holds Our String
va_list ap; // Pointer To List Of Arguments下面两行代码检查是否有需要显示的内容,如果什么也没有,fmt就等于空(NULL),屏幕上也就什么都没有。
if (fmt == NULL) // If There's No Text
return; // Do Nothing接下来三行代码将文字中的所有符号转换为它们的字符编号。最后,文字和转换的符号被存储在一个叫做“text”的字符串中。以后我会多解释一些有关字符的细节。
va_start(ap, fmt); // Parses The String For Variables
vsprintf(text, fmt, ap); // And Converts Symbols To Actual Numbers
va_end(ap); // Results Are Stored In Text感谢Jim Williams对下面一段代码的建议。以前我是用手工将文字置于中心的,而他的办法要好的多。
我们从一个循环开始,它将逐个检查文本中的字符。我们通过strlen(text)得到文本的长度。设置好了循环以后,我们将通过加上每个字符的长度来增加length的值。当循环结束以后,被保存在length中的值就是整个字符串的长度。所以,如果我们要写的是“hello”,假设每个字符的长度都为10个单位,我们先给length的值加上第一个字母的长度10。然后,我们检查第二个字母的长度,它的长度也是10,所以length就变成10 + 10(20)。当我们检查完所有5个字母以后,length的值就会等于50(5 *10)。
给出我们每个字符的长度的代码是gmf[text[loop]].gmfCellIncX。记住,gmf存储了我们每个显示列表的信息。如果loop等于0,text[loop]就是我们的字符串中的第一个字符。如果loop等于1,text[loop]就是我们的字符串中的第二个字符。gmfCellIncX告诉我们被选择的字符的长度。GmfCellIncX表示显示位置从已绘制上的上一个字符向右移动的真正距离,这样,字符之间就不会重叠在一起。同时,这个距离就是我们想得到的字符的宽度。你还可以通过gmfCelllncY命令来得到字符的高度。如果你是在垂直方向绘制文本而不是在水平方向时,这会很方便。
for (unsigned int loop=0;loop<(strlen(text));loop++)// Loop To Find Text Length
{
length+=gmf[text[loop]].gmfCellIncX;//IncreaseLength By Each Characters Width
}
最后我们取出计算后得到的length,并把它变成负数(因为我们要将文本从屏幕中心左移从而把整个文本置于屏幕中间)。然后我们把length除以2。我们并不想移动整个文本的长度,只需要一半!
glTranslatef(-length/2,0.0f,0.0f); // Center Our Text On The Screen然后我们将GL_LIST_BIT压入属性堆栈,它会防止glListBase影响到我们的程序中的其它显示列表。
glListBase(base)函数用来告诉OpenGL到哪里去找每个字符对应的显示列表。
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits
glListBase(base); // Sets The Base Character to 32现在OpenGL知道字符的存放位置了,我们就可以让它在屏幕上显示文字了。GlCallLists会调用多个显示列表从而把整个文字的内容同时显示在屏幕上。
下面的代码做后续工作。首先,它告诉OpenGL我们将要在屏幕上显示出显示列表中的内容。Strlen(text)函数用来计算我们将要显示在屏幕上的文字的长度。然后,OpenGL需要知道我们允许发送给它的列表的最大值。我们依然不能发送长度大于255的字符串。所以我们使用UNSIGNED_BYTE。(用0 - 255来表示我们需要的字符)。最后,我们通过传递字符串文字告诉OpenGL显示什么内容。
也许你想知道为什么字符不会彼此重叠堆积在一起。那时因为每个字符的显示列表都知道字符的右边缘在那里,在写完一个字符后,OpenGL自动移动到刚写过的字符的右边,在写下一个字或画下一个物体时就会从GL移动到的最后的位置开始,也就是最后一个字符的右边。
最后,我们将GL_LIST_BIT属性弹出堆栈,将GL恢复到我们使用glListBase(base)设置base之前的状态。
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);//DrawsThe Display ListText
glPopAttrib(); // Pops The Display List Bits
}重置尺寸的代码与第一课中的代码完全相同,所以在这里省略它。
在InitGL代码的最后有几行新的代码。第13课中的BuildFont()还在,这些新的代码用来制造快速并且不很鲜明的光线。Light0是大多数显卡中预先定义过的,它可以很好的照亮场景而且不影响我的代码。
我还添加了glEnable(GL_Color_Material)命令.因为字是3D对象,因此需要打开材质色彩,否则使用glColor3f(r,g,b)改变颜色时就不能改变文字的颜色。如果你要在屏幕上绘制文本,那么再此之前先打开材质色彩,写完之后,在将它关闭,不然屏幕中的所有物体都会被上色。
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
glShadeModel(GL_SMOOTH); // Enable Smooth Shading
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);//Really Nice Perspective Calculations
glEnable(GL_LIGHT0) // Enable Default Light (Quick And Dirty)
glEnable(GL_LIGHTING); // Enable Lighting
glEnable(GL_COLOR_MATERIAL); // Enable Coloring Of Material
BuildFont(); // Build The Font
return TRUE; // Initialization Went OK
}下面就是画图的代码了。我们从清除屏幕和深度缓存开始。我们调用glLoadIdentity()来重置所有东西。然后我们将坐标系向屏幕里移动十个单位。轮廓字体在透视图模式下表现非常好。你将文字移入屏幕越深,文字开起来就更小。文字离你越近,它看起来就更大。
也可以使用glScalef(x,y,z)命令来操作轮廓字体。如果你想把字体放大两倍,可以使用glScalef(1.0f,2.0f,1.0f). 2.0f 作用在y轴, 它告诉OpenGL将显示列表的高度绘制为原来的两倍。如果2.0f作用在x轴,那么文本的宽度将变成原来的两倍。
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//Clear Screen And Depth Buffer
glLoadIdentity(); // Reset The Current Modelview Matrix
glTranslatef(0.0f,0.0f,-10.0f); // Move One Unit Into The Screen
在向屏幕里移动以后,我们希望文本能旋转起来。下面3行代码用来在3个轴上旋转屏幕。我将rot乘以不同的数,以便每个方向上的旋转速度不同。
glRotatef(rot,1.0f,0.0f,0.0f); // Rotate On The X Axis
glRotatef(rot*1.5f,0.0f,1.0f,0.0f); // Rotate On The Y Axis
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Rotate On The Z Axis
下面是令人兴奋的颜色循环了。照常,我们使用唯一递增的变量(rot)。颜色通过使用COS和SIN来循环变化。我将rot除以不同的数,这样每种颜色会以不同的速度递增。最终的效果非常好。
// Pulsing Colors Based On The Rotation
glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),1.0f-0.5f * float(cos(rot/17.0f)));我最喜欢的部分,将文字写到屏幕上。我使用同将位图字体写到屏幕上相同的函数。将文字写在屏幕上,所有你要做的就是glPrint(“你想写的文字”)。很简单。
在下面的代码中,我们要写的是NeHe,空格,破折号,空格,然后是rot的值除以50后的结果(为了减慢计数器)。如果这个数大于999.99,左边第四个数将被去掉(我们要求只显示小数点左边3位数字)。只显示小数点右边的两位数字。
glPrint("NeHe - %3.2f",rot/50); // Print GL Text To The Screen然后增大旋转变量从而改变颜色并旋转文字。
rot+=0.5f; // Increase The Rotation Variable
return TRUE; // Everything Went OK
}
最后,如下所示,就是在KillGLWindow()函数最后增加KillFont()函数,这很重要,它在我们退出程序之前做清理工作。
if (!UnregisterClass("OpenGL",hInstance)) // Are We Able To Unregister Class
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Set hInstance To NULL
}
KillFont(); // Destroy The Font 在这节课结束的时候,你应该已经学会在你的OpenGL程序中使用轮廓字体了。就像第13课,我曾在网上寻找一篇与这一课相似的教程,但是也没有找到。或许我的网站是第一个涉及这个主题同时又把它解释的简单易懂的C代码的网站吧。享用这篇教程,快乐编码!