计算机图形:输出图元

图元概念

CG API 计算机图形应用编程接口:用于图形应用的通用软件包,提供C++等用于创建图形的函数库。

图形的组成可能包含多个元素,如树木、地形、汽车等,而图形软件包中用来描述各种图形元素的函数,称为图形输出原语(graph output primitive),或图元(primitive)。描述对象几何要素的输出图元,称为几何图元(geometric primitive)。

最简单几何图元:点、直线段;复杂的:圆、二次曲线、二次曲面、样条曲线、曲面等。


坐标系统

为描述图形对象的位置,需要一个2D/3D世界坐标系(标准笛卡尔坐标系)。

坐标范围(coordinate extent):对象坐标x、y、z的最小值、最大值等描述的场景范围,是一个对象的包围盒(bounding box);对2D图形,是一个包围矩形(bouding rectangle)。

屏幕坐标

视频监视器的位置,使用屏幕坐标系(screen coordinate)描述,与帧缓存像素位置相对应。像素坐标值,给出扫描行号y、列号x。

  • 原点
    设备坐标系:屏幕刷新等硬件处理一般从左上角开始对像素进行编址。
    屏幕坐标系,可以用软件命令按任何方式设定屏幕位置(原点可左上,可左下)。

OpenGL中指定2D坐标系统

OpenGL中如何指定物体的坐标?
下面代码在OpenGL中指定2D世界坐标系下,能显示的坐标范围:

glMatrixMode(GL_PROJECTION); // 将当前矩阵指定为投影矩阵
glLoadIdentity();            // 将矩阵设为单位矩阵
gluOrtho2D(xmin, xmax, ymin, ymax); // 指定屏幕区域对应的模型坐标范围, 我们绘制的图形坐标范围必须在这个放完内, 否则看不到

单位矩阵:对角线全位1,其他位位0。如,

[1 0 0
 0 1 0
 0 0 1]

gluOrtho2D将显示窗口左下角坐标设为(xmin, ymin),右上角(xmax, ymax)。如果不调用,默认范围X: -1~1, Y: -1~1。
tips:建立图形的几何描述时,图元位置需用绝对坐标(世界坐标系)给出。

OpenGL画点函数

如何用OpenGL画一个点?
确认位置,然后指定其他属性。如果不指定属性,则OpenGL图元按默认大小、颜色显示。默认图元颜色:白色;点大小:屏幕像素大小。

// 画一个点

glBegin(GL_POINTS);
glVertex*(); // 指定点位置
glEnd();

glVertex* 星号表示该函数后缀码,指明空间维数(如2/3/4)、坐标值类型(如i: int, s: short, f: float, d: double)、向量形式坐标(如v)。
glVertex 须位于glBegin和glEnd之间。glBegin参数指定输出图元的类型,GL_POINTS表示要输出图元是点。

e.g. 2D坐标系下,画3个点:

glBegin(GL_POINTS);
glVertex2i(50, 100);
glVertex2i(75, 150);
glVertex2i(100, 200);
glEnd();

点坐标->向量形式:

int point1[] = {50, 100};
int point2[] = {75, 150};
int point3[] = {100, 200};

glBegin(GL_POINTS);
glVertex2iv(point1);
glVertex2iv(point2);
glVertex2iv(point3);
glEnd();

3D坐标系下,画2个点:

glBegin(GL_POINTS);
glVertex3f(-78.05, 909.72, 14.60);
glVertex3f(261.91, -5200.67, 188.33);
glEnd();

class/struct存储坐标:

struct wcPt2D {
    GLfloat x, y;
};

然后用OpenGL函数绘制:

wcPt2D pointPos;
pointPos.x = 120.75;
pointPos.y = 45.30;

glBegin(GL_POINTS);
glVertex2f(pointPos.x, pointPos.y);
glEnd();

OpenGL画线

一条线段由2个端点确定。OpenGL中,可用glVertex选择端点坐标;而绘制的方式(由符号常量指定)有3种:GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP。

  • GL_LINES 连接每一对相邻端点,得到一组直线段;如果只描述端点,则什么也不会显示。i.e. 奇数个端点,则最后一个端点不显示
glBegin(GL_LINES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
  • GL_LINE_STRIP 获得折线(polyline),从第一个端点到最后一个,首尾相连(非闭合)。
glBegin(GL_LINE_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
  • GL_LINE_LOOP 获得封闭折线(closed polyline),从第一个端点到最后一个,首尾相连(闭合)。
glBegin(GL_LINE_LOOP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();

上面3种符号常量,分别对应下面3个图:

tips:默认情况,每个符号常量显示白色实现。


填充区图元

颜色/图案填充区域,是一种可描述图形组成的结构,称为填充区(fill area)或填充的区域(filled area)。可描述实体表面,通常是平面,主要是多边形。

用一组多边形面片描述的对象,称为标准图形对象(standard graphics object)或图形对象(graphs object)。


多边形填充区

多边形(polygon)定义:由3个或更多顶点描述的平面图形,这些顶点由边(edge or side)顺序连接。
根据定义,
1)多边形所有顶点在一个平面;
2)所有边无交叉;
3)所有边形成封闭折线;

多边形分类

多边形中,
内角(interior angle):两条相邻边形成的多边形边界内的角。
凸(convex)多边形:一个多边形所有内角 < 180°。
凹(concave)多边形:不是凸多边形的多边形。
**退化多边形(degenerate polygon):常用于描述共线或重叠位置的顶点集。

为了软件鲁棒性,图形软件包可拒绝退化或非平面的顶点集。这要求程序员进行额外的处理。

识别凹多边形

特征:
1)至少有一个内角 > 180°;
2)某些边的延长线与其他边相交;
3)某些内点之间连线与边相交;

可用相邻边向量的叉积(叉积是一个向量,与标量点积不同)测试凹凸性:凸多边形的所有向量叉积均同号(或0);如果叉积有正有负,则确定为凹多边形。

注:图中V代表顶点,E代表边

Hints:向量e1=(x1, y1), e2=(x2,y2), 夹角θ,则,
1)叉积的数值为e1 x e2 = |e1||e2|sinθ,
2)方向遵循右手定则:垂直e1,e2所在平面,按右手从e1转向e2,大拇指方向就是叉积方2向,即z分量方向。
垂直纸面向外,z分量 > 0;垂直纸面向内,z分量 < 0。

分割凹多边形

为什么要分割凹多边形?
因为凹多边形的填充算法比较复杂,可将其分割成一组凸多边形,然后进行处理,能提高效率。

如何分割?

  • 1)向量法
    假定多边形在xy平面上(即使不在,可变换得到),对于分割凹多边形的向量方法(vector method),首先要形成边向量。给定相继的向量位置Vk和Vk+1,定义边向量
    Ek = Vk+1 - Vk
    按顺序从第一条边到最后一条,计算连续边向量的叉积。如果有些叉积z分量为正,而另一些为负,则多边形为凹多边形;否则,为凸多边形。
    因此,可将连续叉积z分量符号相同的边 分割为同一组凸多边形。

  • 2)旋转法
    沿多边形的边逆时针方向,逐一将顶点Vk移动到坐标原点,然后顺时针旋转多边形,使下一顶点Vk+1落在x轴。如果再下一顶点Vk+2位于x轴下面,则多边形为凹;如果所有顶点都通过测试,则为凸。

当测试不通过,即为凹的点时,将对应边(位于x轴)延长,必与多边形另一条边相交,从而分割多边形。

将凸多边形分割成三角形集

三角形是最简单的多边形,图形软件包都持支持三角形的绘制。实践中,可将任意凸多边形分割成三角形集。

分割方法:
设凸多边形顶点集U={V1, V2, ..., Vn}(相邻点按顺序排列,起始点任意,n>=3),
1)从第一个点V1开始,连续3个点定义为一个新三角形△V1V2V3,同时从顶点集U删去△中间点V2,剩余顶点集U1={V1,V3,...,Vn};
2)对剩余顶点集U1重复步骤1)操作,直到剩余顶点数 == 3,即为最后一个△;

该方法也适用于凹多边形分割为三角形集,但要求连接的第一、三顶点线段不能穿过多边形的凹区域,且每次形成△内角 < 180°。

多边形的内-外测试

有时需要鉴别对象的内部区域,如何进行?(注意:对象的内部区域是封闭区域,空间有限)
对于2D平面下的多边形对象,有2种规则:

  • 奇偶规则(odd-even rule),也叫奇偶性规则(odd-parity rule)或偶奇规则(even-odd rule)

从任意位置P到对象坐标范围以外的远点画一条射线,并统计该射线与各边的交点数目。假如与这条射线相交的多边形的边数为奇,则P是内部(interior)点;否则,P是外部(exterior)点。

注意:所画射线不与多边形顶点相交。

  • 非零环绕数(nonzero winding-number)规则

统计多边形边以逆时针方向环绕某一特定点的次数。该数称环绕数(winding-number),2D对象的内部点是那些具有非零环绕数的点。

具体做法:先按某一固定方向将每条边标上方向(如边向量),然后从任意位置P到对象坐标范围外远点画一条射线。但P沿射线方向移动时,统计穿过该射线的边的方向。当边从右到左穿过该射线时,边数+1;从左到右时,边数-1。所有穿过的边都计数后,如果环绕数非0,则P定义为内部点;否则,P为外部点。

注意:所画射线不能与多边形顶点相交,且多边形的边不能竖直方向(无法判断从左到右,还是从右到左)。

下面是自相交封闭折线围成的内部(阴影)和外部区域(空白)示意图:

注意:两种方法得到的内部、外部区域不一定完全相同

有效的确定有向边界穿越射线的方法:
1)叉积法
沿对象边建立边向量(或边界线)E,将边向量E与P点发出的射线向量u进行叉积。假设多边形对象位于xy平面,这叉积方向为+z或-z。如果对于特定边,叉积方向+z代表从右到左穿越射线,环绕数+1;否则,从左到右穿越,环绕数-1。

2)点积法
用点积替换叉积。如果边向量E从右到左穿越射线u时,点积A = |E||u|cosθ,两者夹角θ∈(0, 90°),点积为正,环绕数+1;那么,从左到右穿越射线时,θ∈(90°, 180°),点积A为负,环绕数-1。

多边形表

场景中的对象一般用一组多边形面片来描述,而面片包括:几何信息 + 其他表面参数(颜色、透明度、光反射特性等)。
可将对象的几何数据简单组织为3张表:顶点表、边表、面片表(简称面表)。

  • 顶点表 存储对象的每个顶点;
  • 边表 指向顶点表的顶点,以确定每一多边形的边的端点;
  • 面片表 指向边,以确定每个多边形的边;

下面是表示一个对象表面(含2个相邻多边形)的相关表:

通常,对象及其组成的多边形均赋以对象和面片标识,便于引用它们。

安排3个表格的做法,好处有几点:
1)为引用各组成部分(顶点、边、面片)提供方便;
2)避免重复存储坐标信息;
3)高效显示对象;

还可以对数据表进行扩展,添加附加信息以提高信息的提取速度。例如,扩充边表包含面片表的指针,便于快速判断边属于哪个面片。

通常还将一些几何信息,如每条边的斜率,边、面片及对象的坐标范围,存放在数据表中,能极大提高处理效率;当然,也带来了一致性和完整性的维持开销。

对于三个数据表的错误检查,图形软件包完成的测试有:
1)每个顶点至少有2条边作为其端点;
2)每条边至少是一个多边形的组成部分;
3)每个多边形都是封闭的;
4)每个多边形至少有一条共享边;(注:不明白,如何算共享边?)
5)如果边表包含指向多边形的指针,那么由多边形指针引用的每条边都有一个反向指针指回该多边形。

平面方程

  • 3点确定平面方程

场景中,每个多边形包含在一个无限平面中。而平面一般方程:
Ax + By + Cz + D = 0
其中,(x, y, z)是平面上任意一点,系数A、B、C、D(平面参数,plane parameter)是描述平面特征的常数。

可用多边形上3个不共线的连续顶点,得到3个平面方程,从而确定常数A、B、C、D值:
(A/D)xk + (B/D)yk + (C/D)zk = -1, k=1,2,3

根据克拉默法则,以行列式求出(适用于D=0):

A = |1 y1 z1|
    |1 y2 z2|
    |1 y3 z3|

B = |x1 1 z1|
    |x2 1 z2|
    |x3 1 z3|

C = |x1 y1 1|
    |x2 y2 1|
    |x3 y3 1|

D = -|x1 y1 z1|
     |x2 y2 z2|
     |x3 y3 z3|

前向面与后向面

  • 点、面片的位置关系

对象的多边形的每个面有两侧,向着对象内部的一侧称为后向面(back face),可见或朝外的一侧称为前向面(front face)。

判定一个点相对于前向面、后向面的空间位置,是图形算法的基本任务。任何一个不在平面上且可看见对象前向面的点,称为在平面的前方(或外部),此时该点在对象外部;任何可看见多边形后向面的点,称为在平面的后方(或内部)。位于多边形所在平面后方(内部)的点,是对象的内点。

这种内外部分类方法,与前面环绕数/奇偶规则的内外测试,有何不同?
内外点分类,是相对于多边形的平面的(3D空间);而前面的测试,是针对某些二维边界的内部(相对于对边,2D空间)。

有了定义,具体如何判定前向面 or 后向面?
可以用平面方程,判定一个点与(对象的多边形)面片的相对位置关系。
对于任意点(x,y,z),如果不在A、B、C、D参数确定的平面上,则有:Ax + By + Cz + D ≠ 0.
当Ax + By + Cz + D < 0时,点(x,y,z)在平面后方;
当Ax + By + Cz + D > 0时,点(x,y,z)在平面前方;

  • 法向量

多边形表面的空间方向可以平面的法向量(normal vector)N来描述。法向量是垂直于平面、由平面的内部指向外部且模为1的向量。

法向量可通过向量叉积得到。
假设我们有一个凸多边形面片和一个右手坐标系,从多边形任选3个顶点:V1,V2,V3,满足从对象外部到内部观察时的逆时针排序。这样,形成2个向量(V2-V1)和(V3-V2),进行向量叉积可得到法向量N:
N = (V2-V1)x(V3-V1)
记V1=(x1,y1,z1),V2=(x2,y2,z2),V3=(x3,y3,z3), x,y,z轴方向单位法向量为i=(1,0,0),j=(0,1,0),z=(0,0,1)
V2-V1 = (x2-x1,y2-y1,z2-z1) = (ax, ay, az)
V3-V1 = (x3-x1,y3-y1,z3-z1) = (bx, by, bz)
其中,ax = x2-x1, ay = y2-y1, az = z2-z1; bx = x3-x1, by = x2-x1, bz = x3-x1

根据叉积的坐标运算,可得
N = (ax,ay,az)x(bx,by,bz)
= |i j k |
|ax ay az|
|bx by bz|


OpenGL多边形填充区函数

OpenGL中,填充区必须为凸多边形。一个填充多边形的顶点集,至少包含3个顶点,无相交边且内角均 < 180°。

OpenGL中,多边形填充主要有两种方式:
1)特殊矩形函数glRect*,在xy平面描述顶点,比glVertex更高效:

// 矩形一个角位于(x1,y1),对角(x2,y2),边平行于xy轴
// 后缀码*, 与glVertex*类似, 指出坐标数据类型、是否使用数组元素表达坐标
// 生成矩形时,按顺时针生成顶点序列:(x1,y1),(x2,y1),(x2,y2),(x1,y2)
glRect*(x1, y1, x2, y2)

下面代码绘制一个填充正方形:

// 后缀码i
glRecti(200, 100, 50, 250);

// 后缀码iv
int vertex1[] = {200, 100};
int vertex2[] = {50, 250};
glRectiv(vertex1, vertex2);

2)glBegin传入6种图元常量,为多边形指定不同填充方式,一组glVertex命令指定顶点。

  • 三角形图元常量:GL_POLYGON, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN

示例:给定六边形6个顶点,用不同图元常量进行填充绘制。
假设2D平面下,6个顶点(p1到p6)按多边形边的逆时针次序排列。下面代码用了4种图元常量:

// 近似正六边形6个顶点
int p1[] = { 0, 100 };
int p2[] = { 50, 13};
int p3[] = { 150, 13 };
int p4[] = { 200, 100 };
int p5[] = { 150, 187 };
int p6[] = { 50, 187 };

// 填充多边形区域, 对同一套顶点, 但不同图元常量进行填充

// 单个多边形, 见(a)
glBegin(GL_POLYGON);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glEnd();

// 每3个顶点配对成三角形, 6个顶点共2个三角形, 见(b)
glBegin(GL_TRIANGLES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
#endif

// 连续3个点配对成三角形, 6个顶点共4个三角形, 见(c)
glBegin(GL_TRIANGLE_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p5);
glVertex2iv(p4);
glEnd();

// 以p1为起点, 后面连续2个点与p1配对成三角形, 6个顶点共4个三角形, 见(d)
glBegin(GL_TRIANGLE_FAN);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glEnd();
  • 四边形图元常量:GL_QUADS, GL_QUAD_STRIP

GL_QUADS:类似于GL_TRIANGLES,每4个顶点对应一个四边形,8个点共2个(独立的)四边形;
GL_QUAD_STRIP:类似于GL_TRIANGLE_STRIP,每4个顶点对应一个四边形,8个点共3个四边形,后面的四边形会跟前面的共享2个顶点。

OpenGL顶点数组

假设现在要绘制一个3D单位立方体,6个顶点是这样:

typedef GLint vertex3 [3];

vertex3 pt[8] = {
    {0, 0, 0}, {0, 1, 0}, {1, 0, 0}, {1, 1, 0},
    {0, 0, 1}, {0, 1, 1}, {1, 0, 1}, {1, 1, 1}
};

如果按前面的方法,就要定义6个面,分6次调用glBein(GL_POLYGON)或glBegin(GL_QUADS)。

// 传入四边形, n1,n2,n3,n4对应实参顺序必须为从立方体外到内观察时 逆时针的属性
void quad(GLint n1, GLint n2, GLint n3, GLint n4)
{
    glBegin(GL_QUADS);
    glVertex3iv(pt[n1]);
    glVertex3iv(pt[n2]);
    glVertex3iv(pt[n3]);
    glVertex3iv(pt[n4]);
    glEnd();
}
void cube()
{
    quad(6, 2, 3, 7);
    quad(5, 1, 0, 4);
    quad(7, 3, 1, 5);
    quad(4, 0, 2, 6);
    quad(2, 0, 1, 3);
    quad(7, 5, 4, 6);
}

立方体顶点对应数组pt的索引值:

指定一个面要调用6个OpenGL函数,6个面就是36个。显然,面对复杂物体时,会调用更多OpenGL函数。

指定一个面要调用6个OpenGL函数,6个面就是36个。显然,面对复杂物体时,会调用更多OpenGL函数。

有没有更简单的办法,减少OpenGL函数调用?
有,为解决该问题,OpenGL提供顶点数组(vertex array)。

使用步骤:
1)调用glEnableClientState(GL_VERTEX_ARRAY)激活顶点数组特性;
2)使用glVertexPointer指定顶点坐标的位置和数据格式;
3)使用子程序如glDrawElements显示场景,可处理多个图元而仅需少量函数调用;

绘制立方体程序可简化成:

glEnableClientState(GL_VERTEX_ARRAY); // 激活客户/服务器系统中客户端能力(此时时顶点数组)
glVertexPointer(3, GL_INT, 0, pt);    // 传入顶点数据

// 立方体6个面的顶点索引, 共对应24个点
GLubyte vertIndex[] = { 6, 2, 3, 7, 
    5, 1, 0, 4,
    7, 3, 1, 5,
    4, 0, 2, 6,
    2, 0, 1, 3,
    7, 5, 4, 6,
    };
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, vertIndex);

glDisableClientState(GL_VERTEX_ARRAY)与glEnableClientState相反,使指定特性无效。

glVertexPointer:提供对象顶点的位置和格式。第一个参数(3),指出每个顶点描述中的坐标数目;第二个参数OpenGL常量(GL_INT),指定顶点坐标的数据类型是整型;第三个参数给出连续顶点之间的字节位移,由于这里没有颜色、不透明度,因此坐标是连续的,值为0;最后一个表示指向包含坐标的顶点数组pt。

glDrawElements:显示图元。第一个参数是图元常量;第二个参数是顶点总数,四边形4个顶点,6个四边形就是24个;第三个参数GL_UNSIGNED_BYTE,指定索引的类型,另2种可用索引类型:GL_UNSIGNED_SHORT和GL_UNSIGNED_INT;最后一个参数是图元对应的顶点索引。

其他信息也能与坐标一起放进顶点数组,用于场景描述。见章节:实现图元及属性的算法


像素阵列图元

除几何数据外,OpenGL还能渲染2种重要类型的数据:

  • 位图,一般用于表示字体中的字符;
  • 图像数据,可以被扫描或计算。

位图(bitmap)和图像数据都用矩形的像素阵列表示。区别是位图的每个像素是2值的,只用1bit存储;图像的每个像素包含了颜色(红、绿、蓝、alpha)。一个彩色像素阵列称为一个像素图(pixmap)。

像素阵列的参数包括:
1)指向屏幕的指针;
2)矩阵的大小;
3)将要影响的屏幕区域。

位图

位图是由0和1组成的矩形数组,作为窗口中的一个矩形区域的绘图掩码。

假设我们正在绘制一幅位图,并且当前光栅颜色是红色。在位图中值为1的地方,帧缓冲区中的对应像素用红色代替;值为0的地方,就不会生成片段,像素内容不受影响。此时,位图相当于掩码。位图常用功能是在屏幕上绘制字符。

OpenGL对字符串的绘制、字体的操纵,只提供了最底层的支持。通过glRasterPos*()定位位图,glBitmap()绘制位图。

  • 当前光栅位置

当前光栅位置(current master position)是开始绘制下一幅位图(或图像)的屏幕位置。位图在将原点(左下角)置于当前光栅位置后显示。通过glRasterPos*设置位图的当前光栅位置。

要在光栅位置(20,20)开始绘制位图,可调用glRasterPos2i设置:
glRasterPos2i(20, 20);
如果想使用窗口坐标,而不是屏幕坐标,可以使用glWindowsPos*()。

可以用glGetFloatv() + GL_CURRENT_RASTER_POSITION获取当前光栅位置。

  • 绘制位图

可以用glBitmap绘制bitmap指定的位图。bitmap是指向位图图像的指针,width和height是位图的宽、高,位图的原点是当前光栅位置 - (xb0, ybo),而(xbi, ybi)决定了下一个当前光栅位置。

void glBitmap(GLsizei width, GLsizei height, 
              GLfloat xb0, GLfloat ybo, 
              GLfloat xbi, GLfloat ybi,
              const GLubyte *bitmap);
  • 选择位图颜色

可以用glColor()设置当前颜色。注意需要在glRaster之前调用。

示例:下面代码将绘制3个位图"F"。

GLubyte rasters[24] = { // F对应的bitmap
    0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,
    0xff,0x00,0xff,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,
    0xff,0xc0,0xff,0xc0,
};

void init(void)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, winWidth, 0.0, winHeight);
    
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 设置内存中像素对齐方式
    glClearColor(1.0, 1.0, 1.0, 1.0);
}

void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1.0, 0.0, 0.0); // red
    glRasterPos2i(20, 20);
    glBitmap(10, 12, 0.0, 0.0, 11.0, 0.0, rasters);
    glBitmap(10, 12, 0.0, 8.0, 11.0, 0.0, rasters);
    glBitmap(10, 12, 0.0, -8.0, 11.0, 0.0, rasters);
    glFlush();
}

图像

图像与位图相似,但每个像素都可以存储完整的颜色(R/G/B/A),也称彩色阵列。图像可用于显示、纹理贴图。

通常,图像来源于:
1)颜色缓冲区的图片;
2)深度缓冲区和模板缓冲区读取(或写入)的矩形的像素数据。

OpenGL提供3个基本函数读取、写入、复制像素数据:glReadPixels、glDrawPixels、glCopyPixels。对像素数据的操作,也称为光栅操作(raster operation)。

  • glReadPixels():从帧缓冲区读取一个矩形像素数组,并把数据保存在内存中。

从帧缓冲区的一个矩形区域读取像素数据,矩形区域的左下角位于窗口坐标(x, y),宽高分别为width, height。读取的像素数据保存到pixels数组。format表示像素数据元素的类型(索引或RGBA),type表示每个元素的数据类型。

void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height,
                  GLenum format, GLenum type, GLvoid *pixels);
  • glDrawPixels():把内存中保存的一个矩形像素数组写入到帧缓冲区中由glRasterPos*指定的当前(光栅)位置。

绘制一个宽和高度为width和height的矩形像素数据。矩形左下角是当前光栅位置。format、type含义同glReadPixels的参数,pixels数组包含要绘制的像素数据。

void glDrawPixels(GLsizei width, GLsizei height, GLenum format, GLenum type, const GL void *pixels);

OpenGL提供若干缓存,将某缓存选为glDrawPixels的目标,即可将一个阵列送进缓存。不同的缓存存放不同的数据:有的缓存存放颜色值,有的存放另外的像素数据,如深度缓存(depth buffer)存放对象离开观察位置的距离,模板缓存(stencil buffer)存放场景的边界图案。format设为GL_DEPTHA_COMPONENT或GL_STENCIL_INDEX,就可在2个缓存中选一个。

  • glCopyPixels():把一个矩形像素数组从帧缓冲区的一部分复制到另一部分。行为类似于调用glReadPixels() + glDrawPixels(),但数据并不会写入内存中。

从帧缓冲区中的一个矩形区域复制像素数据到当前光栅位置,该矩形左下角(x, y),宽高width, height。buffer是GL_COLOR, GL_STENCL或GL_DEPTH,指定函数所使用的帧缓冲区。

void glCopyPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum buffer);

示例:下面程序利用glDrawPixels绘制一个8x8黑白相间的棋盘网格。

#define checkImageWidth 64
#define checkImageHeight 64
GLubyte checkImage[checkImageHeight][checkImageWidth][3];
GLsizei winWidth = 480, winHeight = 320;

void makeCheckImage(void)
{
       int i, j, c;
       for (i = 0; i < checkImageHeight; i++) {
              for (j = 0; j < checkImageWidth; j++) {
                     c = (((i & 0x8) == 0) ^ ((j & 0x8) == 0)) * 255;
                     checkImage[i][j][0] = (GLubyte)c;
                     checkImage[i][j][1] = (GLubyte)c;
                     checkImage[i][j][2] = (GLubyte)c;
              }
       }
}
void init()
{
       glMatrixMode(GL_PROJECTION);
       glLoadIdentity();
       gluOrtho2D(0.0, winWidth, 0.0, winHeight);
       glClearColor(1.0, 1.0, 1.0, 1.0);
       glShadeModel(GL_FLAT); // GL_SMOOTH GL_FLAT
       makeCheckImage();
       glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
}
void display()
{
       glClear(GL_COLOR_BUFFER_BIT);
       glRasterPos2i(0, 0);
       glDrawPixels(checkImageWidth, checkImageHeight, GL_RGB,
              GL_UNSIGNED_BYTE, checkImage);
       glFlush();
}

字符图元

图形软件包提供生成字符图元的子程序,用来显示字母、数字、其他字符,可显示成不同的大小、风格。一组字符的完整设计风格,称为字样(typeface),常用有Courier、Helvetica、New York等。现在,字体和字符经常互换使用。

字体分2大类:有衬线(serif)、无衬线(sans serif)。
区别:
1)有衬线字体在字符主笔划末端带有细线或笔划加重,无衬线没有加重;
2)有衬线字体可读性较好,容易阅读,适合段落文字;无衬线字体的单个字符容易被识别,适合标识和标题。

计算机如何存储字体?
2种方法:
1)使用矩形网格图案表示某种字体字符形状,这种字符组称为位图字体(bitmap font)。位图化字符集也成光栅字体(raster font);
2)用直线和曲线段描述字符描述字符形状,这种字符组称为轮廓字体(outline font)或笔划字体(strok font)。

下图是字符B的两种字符表示方法:
(a)8x8二值点阵图案表示的位图字体;(b)直线+曲线表示的轮廓字体

  • 优缺点
    位图字体:优点是简单、高效;缺点是每种尺寸和格式的变化都需要存储在高速缓存中,更多的存储空间,而且很难生成不同尺寸、粗体、斜体等其他变体的字体。
    轮廓字体:更少的存储空间,容易产生粗体、斜体、不同尺寸字体;缺点:需要更多时间处理轮廓字体。

OpenGL如何显示字符?
提供2类函数:
1)接收任意的字符串、相应的帧缓存起始位置;
2)仅在选定的一处或几处显示字符串。

同其他图元,字符的几何描述在世界坐标系中给出,然后由观察变换映射到屏幕坐标系。

OpenGL字符函数

  • 库支持

OpenGL基本库仅支持显示单个字符和文字串。将常用的位图字符作为字库存储,一个文字串从字库中选择的位图序列映射到帧缓存的相邻位置来显示。

GLUT(实用函数工具包)有些预定义的字库,因此用户不用创建位图字形库,除非要显示没有的字体。GLUT可显示位图和轮廓字体,位图字体由glBitmap()绘制,轮廓字体由折线边界(glBegin(GL_LINE_STRIP))生成。

  • 显示GLUT位图字符

glutBitmapCharacter显示GLUT位图字符,显示字符以当前光栅位置作为位图原点(左下角)。在字符位图装入刷新缓存后,当前光栅位置的x坐标+一个字符宽度的增量。
glutBitmapCharacter(font, character);
font: GLUT常量,指定特定字形集。GLUT_BITMAP_8_BY_13或GLUT_BITMAP_9_BY_15选择一种固定宽度字体并确定其参数;GLUT_BITMAP_TIMES_ROMAN_10或GLUT_BITMAP_HELVETICA_10选择10磅比例间隔字体;
character:要显示的字符(或ASCII码),如要"A",可传入ASCII码65。

  • 显示位图字符

例,显示一个包括35个位图字符的文字串
glRasterPosition2i(x, y);
for (k = 0; k < 36; k++)
glutBitmapCharacter(GLUT_BITMAP_9_BY_15, text[k]); // 用指定颜色显示字符串

  • 显示轮廓字符

例,显示一个轮廓字符
glutStrokeCharacter(font, character);
font: GLUT_STROKE_ROMAN 显示比例空间的字体,GLUT_STROKE_MONO_ROMAN 显示常量间隔的字体。

字符的大小、位置在执行该函数前,通过指定变换操作来控制。每个字符显示后,自动坐标位移,从而使得下一个字符在当前字符的右边。


显示表(display list)

OpenGL使用称为显示表(display list)的结构,把对象描述成一个命名的语句序列(或任何其他的命令集)并存储起来,方便、高效。建立显示表后,可用不同的显示操作引用该表。

显示表对层次式建模很有用,因为一个复杂的对象可用一组简单的对象来描述。

创建和命名OpenGL显示表

用glNewList/glEndList包围一组OpenGL命令,可形成显示表:

glNewList(listId, listMode);
... // 中间的OpenGL命令会自动加入显示表
glEndList();

listId: 正整数作为表名,来形成一个显示表;
listMode:OpenGL常量GL_COMPILE,为以后执行而存储该表;或GL_COMPILE_AND_EXECUTE,放入表的命令立即执行,但仍然可以在以后再执行。

显示表创建后,立即对包含坐标位置、颜色分量等参数的表示进行赋值计算,从而使表仅存储参数的值。对这些参数的任何后继修改都不起作用,因为不能修改显示表的值,所以显示表不能包含如顶点表指针等OpenGL命令。

  • 创建显示表标识

可创建任意多的显示表,并调用一个标识来执行特定的显示表。显示表还能嵌套在另一个内。如果显示表被赋予已经使用的标识,它取代原来的显示表内容。为避免重用标识造成显示表丢失,可让OpenGL生成新标识:

listID = glGenLists(1);

得到一个未使用的标识。

如果将实参1改为其他正整数,则得到一个未使用的显示表标识段。如glGenLists(6):保留6个连续正整数并将其中第一个赋给变量listID,出错则不能产生所需数量的连续整数,且返回0。

  • 查询显示表标识

可单独查询指定值是否已用做显示表的标识。

glIsList(listID);

返回值GL_TRUE,表示listID已用做某个显示表标识;GL_FALSE,表示尚未被使用。

执行OpenGL显示表

OpenGL显示表是一组预先存储起来的OpenGL命令序列,那么如何让这个命令序列开始执行呢?
可调用下面语句执行单个显示表:

glCallList(listID);

下面代码创建并执行一个显示表。先在xy平面上建立以(200, 200)为中心、半径=150的圆上6个等距顶点描述的规则六边形的显示表。然后调用glCallList显示该六边形。

const double TWO_PI = 2 * M_PI;
GLuint regHex;
GLdouble theta;
GLint x, y, k;

/* Set up a display list for a regular hexagon.
    * Vertices for the hexagon are six equally spaced
    * points around the circumference of a circle.
    */
regHex = glGenLists(1);
glNewList(regHex, GL_COMPILE);
glBegin(GL_POLYGON);
for (k = 0; k < 6; k++) {
    theta = TWO_PI * k / 6.0;
    x = 200 + 150 * cos(theta);
    y = 200 + 150 * sin(theta);
    glVertex2i(x, y);
}
glEnd();
glEndList();

glCallList(regHex);

可调用下面语句执行多个显示表:

glListBase(offsetValue); // 将添加到glCallLists中显示表索引的偏移量. 初始值零

glCallLists(nLists, arrayDataType, listIDArray);

nLists:要执行的显示表数量;
listIDArray:显示表标识的数组,可包含任意多元素,无效标识会被忽略;
arrayDataType:指出listIDArray中元素的数据类型,如GL_BYTE, GL_INT, GL_FLOAT, GL_3_BYTES, GL_4_BYTES;
offsetValue:显示表标识偏移,将listIDArray中一个元素值+offsetValue得到。默认值0。最终的偏移量,还取决于glListBase设置的偏移量。

删除OpenGL显示表

删除连续的一组显示表:
glDeleteLists(startID, nLists);
startID:要删除的起始显示表标识;
nLists:要删除的显示表总数。

e.g. 删除4个显示表,其标识:5、6、7、8.

glDeleteLists(5, 4);

显示窗口重定形

  • 窗口重定形

用鼠标将显示窗口拖到屏幕另一位置,或改变窗口形状,该如何重新显示?

为允许对显示窗口尺寸的改变做出反应,glut提供glutReshapeFunc()重绘窗口,函数可和GLUT函数一起放在main中,在显示窗口尺寸输入后立即激活
glutReshapeFunc(winReshapeFcn);
winReshapeFcn:接受新窗口宽度、高度的过程名。

例子,展示如何构造回调winReshapeFcn:

#include <cmath>
#include <gl/glut.h> // Or others, depending on the system in use

const double TWO_PI = 6.2831853;

/* Initial display-window size. */
GLsizei winWidth = 400, winHeight = 400;
GLuint regHex;

class screenPt
{
private:
    GLint x, y;
public:
    /* Default ctor: initializes coordinate position to (0, 0). */
    screenPt() {
        x = y = 0;
    }
    void setCoords(GLint xCoord, GLint yCoord) {
        x = xCoord;
        y = yCoord;
    }
    GLint getx() const {
        return x;
    }
    GLint gety() const {
        return y;
    }
};

static void init(void)
{
    screenPt hexVertex, cirCtr;
    GLdouble theta;
    GLint k;
    const int radius = 150;

    /* Set circles center coordinates. */
    cirCtr.setCoords(winWidth / 2, winHeight / 2);
    glClearColor(1.0, 1.0, 1.0, 0.0); // Display-window color = white

    /* Set up a display list for a red regular hexagon.
     * Vertices for the hexagon are six equally spaced
     * points around the circuference of a circle.
     */
    regHex = glGenLists(1); // Get an id for the display list.
    glNewList(regHex, GL_COMPILE);
    glColor3f(1.0, 0.0, 0.0); // Set fill color for hexagon to red.
    glBegin(GL_POLYGON);
    for (k = 0; k < 6; k++) {
        theta = TWO_PI * k / 6.0;
        hexVertex.setCoords(cirCtr.getx() + static_cast<GLint>(radius *  cos(theta)),
            cirCtr.gety() + static_cast<GLint>(radius * sin(theta)));
        glVertex2i(hexVertex.getx(), hexVertex.gety());
    }
    glEnd();
    glEndList();
}

void regHexagon(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glCallList(regHex);

    glFlush();
}

void winReshapeFcn(int newWidth, int newHeight)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, (GLdouble)newWidth, 0.0, (GLdouble)newHeight);

    glClear(GL_COLOR_BUFFER_BIT);
}
int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowPosition(100, 100);             // top-left position
    glutInitWindowSize(winWidth, winHeight);
    glutCreateWindow("Reshape-Function & Display-List Example");
    init();
    glutDisplayFunc(regHexagon);
    glutReshapeFunc(winReshapeFcn);

    glutMainLoop();
    return 0;
}

小结

  • 输出图元为使用直线、曲线、填充区域、单元阵列样式和文本构造图形提供了基本的工具。

  • 在笛卡尔世界坐标系中,给出图元的几何描述。

  • 填充区域,是显示成单色/彩色图案的平面区域。填充区域可以是任何边界,不过图形软件包通常仅允许凸多边形;凹多边形填充区域,可分割成一组凸多边形来显示。三角形是最容易填充的多边形。

  • 奇偶规则和非零环绕数规则用来判定平面区域的内点。非零环绕数规则,在处理多个边界定义的对象时更灵活。

  • 每个多边形都有面片所在平面空间的前向面和后向面。该空间方向可用多边形面片法向量来确定。法向量可以从多边形平面方程,或者用平面上逆时针排列且三个夹角<180°的三个点求向量叉积得到。

  • 一个场景的所有坐标值、空间方向、其他几何数据分别放入顶点表、边表、面片表。

  • 其他有用图元还有图案阵列、字符串。图案阵列可用于描述各种2D形状,包括用矩形结构的二值/彩色值几何表达的字符集。字符串用来为图形提供标记。

  • OpenGL核心库图元函数可生成点、直线段、凸多边形填充区和位图或像素图的图案阵列。GLUT有显示字符串的子程序。

  • 除glRect外,顶点、线段或多边形的每一位置均在glVertex函数中指定。定义的每个图元的一组glVertex,都用一对glBegin/glEnd包含,其中图元类型由glBegin参数标识。描述包含许多多边形填充表面时,可用OpenGL顶点数组来制定几何和其他数据,从而高效显示结果。

  • OpenGL中生成输出图元的基本函数:

gluOrtho2d // 指定2D世界坐标系
glVertex*  // 选择一坐标位置。该函数必须位于glBegin/glEnd之间
glBegin(GL_POINTS); // 绘出一个或多个点,每个都在glVertex中指定。该位置串用glEnd结束
glBegin(GL_LINES);  // 显示一组直线段,其断电坐标在glVertex中指定,该端点串用glEnd结束
glBegin(GL_LINE_STRIP); // 显示用与GL_LINES同样的结构指定的折线
glBegin(GL_LINE_LOOP);  // 显示用与GL_LINES同样的结构指定的封闭折线
glRect*                 // 显示xy平面上一个填充区
glBegin(GL_POLYGON);    // 显示一个填充多边形,其顶点在glVertex中给出且由glEnd结束
glBegin(GL_TRIANGLES);  // 显示一组填充三角形,其描述结构与GL_POLYGON相同
glBegin(GL_TRIANGLE_STRIP); // 显示一组填充三角带,其描述结构与GL_POLYGON相同
glBegin(GL_TRIANGLE_FAN); // 显示一扇形填充三角形带。所有三角形都与第一顶点相连,其描述结构与GL_POLYGON相同
glBegin(GL_QUADS);      // 显示一组填充四边形,其描述结构与GL_POLYGON相同
glBegin(GL_QUAD_STRIP); // 显示一组填充四边形带,其描述结构与GL_POLYGON相同
glEnableClientState(GL_VERTEX_ARRAY); // 激活OpenGL的顶点数组设施
glVertexPointer(size,type,stride,array); // 指定一坐标值数组
glDrawElements(prim,num,type,array);  // 从数组数据中显示一指定图元类型
glNewList(listID,listMode); // 把一组命令定义为一个显示表,用glEnd结束
glGenlists         // 生成一个或多个显示表标识
glIsList           // 判断一个显示表标识是否被使用
glCallList         // 执行一个显示表
glListBase         // 指定显示表标识数组的位移
glDeleteLists      // 删除指定的一串显示表
glRasterPos*       // 为帧缓存指定一个2D/3D当前位置。该位置作为位图和像素图图案的参考
glBitmap(w,h,x0,y0,Xshift,yshift,pattern); // 指定要映射到与当前位置对应的像素位置的位图图案
glDrawPixels(w,h,type,format,pattern); // 指定要映射到与当前位置对应的像素位置的像素图图案
glDrawBuffer       // 选择存储像素图的一个或多个缓存
glReadPixels       // 将一块像素存入指定的数组
glCopyPixels       // 将一块像素从一个缓存复制到另一个
glLogicOp          // 在用常量GL_COLOR_LOGIC_OP激活后选择一种逻辑操作来组合2个像素数组
glutBitmapCharacter(font,char); // 选择一种字体和一个位图字符串来显示
glutStorkeCharacter(font, char) // 选择一种字体和一个轮廓字符来显示
glutReshapeFunc    // 指定显示窗口尺寸改变时的工作
posted @ 2023-10-23 13:50  明明1109  阅读(43)  评论(0编辑  收藏  举报