QQ群讲义3--BC3.1的图形学设置及两个经典图形学算法
众所周知,TC和BC这两个软件由于历史的原因,在中国的生命力出了奇的顽强,TC似乎是不少高校学生学C语言的首选,黑乎乎的运行界面,导致了好多人认为TC只能写出这种黑乎乎的程序,当然,TC和BC诞生在那个年代,写Windows程序确实困难(注意,是困难,并不是不可以),而且,即使不写Windows,写个窗口和图形的程序虽然有点麻烦,但也是完全可以的,在学生时代,每个用BC和TC的人可能都想写个图形程序,但成功的不多。
首先,刚才有人说没用过这个BC 3.1,先说一下这个工具的设置。
对于刚安装成功者,主要是四个路径的设置:
点Option--Directories之后出来的界面上,有四个路径,其中Include是h文件的所在地,一般就是安装后的目录下的include文件夹;lib是库文件,其中包含着很多函数的实现过程,也是十分重要的,一般设定为安装目录下的lib文件夹;source是源文件,就是自己集中存放源文件的位置;output是输出文件设置,就是默认的某个程序编译之后,生成的obj文件和exe文件所在地。
由于BC和TC界面十分相似,操作我就不多说了
下边,先简单说明一下BC对图形程序的处理概念,然后说说如何设置,为图形程序服务,再用一个Line函数,实现一个线段。
图形程序,首先涉及的问题就是显卡的驱动,大家都知道,即使是Windows,安装之后,往往还要单独安装显卡驱动,所以,在BC下,这个问题有单独的处理方法:
凡是安好BC的,在BC的那个文件夹下会看到一个BGI文件夹,其中有很多CHR与BGI文件,这个就是咱们用BC做图形程序的根本。
实际上BGI就是BC和显卡之间的一个接口,BGI文件就是BC的图形驱动,由于年代久远,且考虑到通用性,所以这些BGI文件不可能十分充分的发挥显卡的功能,只能在一个中等的层次上去画图,这个文件夹里的ATT、CGI、EGAVGA这些bgi文件都是。
操作方法如下:
打开BC3.1,因为图形程序的链接需要一个设置,在Option-Linker-Library中有个选项Graphics lib这个选项前边大家先选定,注意,BC的选定是个X,默认的是不选的,做图形程序这个一定要选上,好,再说下一步,刚才在BGI文件夹中,大家看到了一个exe文件,“BGIOBJ.exe”它的作用就是把BC的图形驱动转换为lib文件,然后添加到graphics.lib中
使用方法是,启动命令行,用cd命令,转到BC所在的目录,用cd BGI,转到BGI文件夹,然后,输入BGIOBJ EGAVGA,输入这个命令的作用就是把EGA,VGA这两种的驱动文件转换为了一个egavga.obj文件,这个文件先放着, Windows下打开BC/Lib文件夹, 在这个文件夹下会发现一个Tlib的可执行文件,这个文件的作用就是对lib文件和obj文件进行连接,把Tlib这个可执行文件复制到BC/Lib文件夹下,先放着,然后回到咱们刚才那个BC/BGI文件夹下,把egavga.obj文件找到,复制到BC/Lib文件夹下,好了,三方大会师,下边就是至关重要的一步,命令行下,用cd命令转到Lib这个文件夹,然后输入Tlib graphics.lib+egavga.obj,就生成了一个新的graphics.lib文件,这个文件就保证了在调用graphics.h文件中的画图函数以EGA或VGA的显示模式画图的时候可以正常显示,以上是对BC进行了基本的设置
下边就说程序中的方法:
用某文本编辑器或直接在BC中开始写
#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
}
首先,大家写下上述内容,第一个就是图形函数所在的文件,后两个略,在好多书上,往往会看到这样一段示例程序:
int main()
{
//初始化图形程序设置
int gdriver =DETECT, gmode, errorcode;
initgraph(&gdriver, &gmode, "");
cleardevice(); //清空屏幕
//图形错误处理
errorcode = graphresult();
if (errorcode != grOk)
{
printf("Graphics error: %s/n", grapherrormsg(errorcode));
printf("Press any key to halt:");
getchar();
exit(1);
}
setcolor(BLUE);
line(20,50,200,400);
getchar();
return 0;
}
大家谁有兴趣,可以先试试这个程序是否可以运行,估计大家会看到一条经典错误“BGI Error: Graphics not initialized (use 'initgraph')” ,实际上,仔细看有关文档,往往会发现一段话,意思是说,Turbo C对于用initgraph()函数直接进行的图形初始化程序, 在编译和链接时并没有将相应的驱动程序(*.BGI)装入到执行程序, 当程序进行到intitgraph()语句时, 再从该函数中第三个形式参数char *path中所规定的路径中去找相应的驱动程序(上边的说法TC换成BC也是一样的,但在VC是没有<graphics.h>的,<graphics.h>是BC和TC特有的,VC下,画图程序的方式整个变了)。
实际上,在一般我们写的程序中,都是用 ” ” 来表示上边那个initgraph的第三个参数,表示当前路径,当然,直接运行BC,程序源文件,以及程序生成的可执行文件所在的位置都试过了,把egavga.bgi文件放到这三个地方,错误依然出现,说明要有别的方法来解决;于是查了不少文章,终于看到了“独立图形程序”这个概念,仔细看看,知道了原因。就是说这个方法是要建立一个独立执行的文件,方法是:
registerbgidriver(EGAVGA_driver);
这句放在initgraph之前,这句的作用在于把EGAVGA的图形驱动进行注册,结合这些考虑,上边那个问题出现的原因应该是BC在Windows下运行的时候是一个外壳,而不是真正的DOS环境,如果是在DOS下,可能这个注册函数就不需要了,所以,完整的程序应该如下:
#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
//初始化图形程序设置
registerbgidriver(EGAVGA_driver);
int gdriver =DETECT, gmode, errorcode;
initgraph(&gdriver, &gmode, "");
cleardevice(); //清空屏幕
//图形错误处理
errorcode = graphresult();
if (errorcode != grOk)
{
printf("Graphics error: %s/n", grapherrormsg(errorcode));
printf("Press any key to halt:");
getchar();
exit(1);
}
setcolor(YELLOW);
line(20,50,200,400);
getchar();
return 0;
}
大家看一下这个程序,这个函数的功能很简单,就是画一个黄色的线段
介绍一下常见的一些设置函数和画图函数,先说基本的:
1、initgraph,这个函数的作用是对图形系统进行初始化,初始化两个方面的内容,一个是图形驱动的类型,然后是相应的显示模式,举例来说,
这个函数的原型如下:
void far initgraph(int far *gdriver, int far *gmode, char *path);
第一个参数 gdriver是用来说明图形驱动类型的,比如1,是CGA;3是EGA,但实际上似乎不该这么用,实际商用的方法是自动检测的。
int gdriver =DETECT, gmode, errorcode;
initgraph(&gdriver, &gmode, "");
其中DETECT是个固定常数,这样,就会自动调用BC对当前显卡支持的最好格式,顺便再说一下后边的那个gmode,这个参数主要是在前边那个参数选定的时候,下一步细分的类型,比如选定VGA类型的时候,gmode有三个选择,“0,1,
思考题一个:字符模式下的清屏函数是什么??
继续说几个简单的函数:
一,画点函数,原型如下:
void far putpixel(int x, int y, int color);
使用方法就是直接putpixel(100,100,2)
这个就是在100,100的位置画一个绿色的点,当然,画点函数很少用,通常,BC的颜色系统,是调用显卡的寄存器来运行的,一般的分辨率下,显示模式有四种选择,每种选择模式,又有0,1,2,3三个颜色的值供选择,这个是写在显卡的寄存器里的,所以,通过乘法原理,出来了16种颜色,“0-15”每个数字代表了一种颜色,具体数值如下:
BLACK 0 黑色 DARKGRAY 8 深灰
BLUE 1 兰色 LIGHTBLUE 9 深兰
GREEN 2 绿色 LIGHTGREEN 10 淡绿
CYAN 3 青色 LIGHTCYAN 11 淡青
RED 4 红色 LIGHTRED 12 淡红
MAGENTA 5 洋红 LIGHTMAGENTA 13 淡洋红
BROWN 6 棕色 YELLOW 14 黄色
LIGHTGRAY 7 淡灰 WHITE 15 白色
当然,为了调用方便,每个数值都有对应的符号参数,就像前边那个给出的画线程序,用了一个setcolor(YELLOW),YELLOW实际上就是14
setcolor(YELLOW)
setcolor(14)
效果毫无疑问是一样的
二、画线函数
line(100,100,200,300)
表示的就是(100,100)和(200,300)之间画一条线
画线函数,画矩形和椭圆函数等,这些非点的函数,全都不在函数中设置颜色了,设置颜色的权利全都交给了一个单独的函数,那个函数的作用就是设置前景色,就是每次调用那个函数之后,马上会在下一步的画图上起作用,这个前景色设置函数就是之前的那个setcolor。
下边一个问题就是,刚才反复强调前景色,那么自然就有个问题,背景色呢,背景色在BC中也有个单独的函数“setbkcolor”, 这个函数的参数和setcolor是一样的,也是16个。
下一个问题:就是线的类型,刚才的画线,全都是千篇一律,一条细线,如果想要设置自己的线性,也有个专门的函数:
setlinestyle(int linestyle,unsigned upattern,int thickness)
第一个参数linestyle,顾名思义,就是线的风格或形状,有五种选择
SOLID_LINE 0 实线(这是默认的)
DOTTED_LINE 1 点线
CENTER_LINE 2 中心线
DASHED_LINE 3 点画线
USERBIT_LINE 4 用户定义线
thickness这个参数只有两种选择,一点宽和三点宽,大家注意刚才linestyle这个参数的最后一个选择,这个是用户自定义的,当采用这个参数的时候,upattern参数才有实际的意义,这个时候,upattern参数采用十六进制表示,就像Html中设置有关元素的颜色一样,当然,有所不同的是,可以用16位的二进制数来理解这个,四个四个一组oxf
三、继续来学习几个图形的算法:
先来DDA,然后是Cohen-Sothurland
(一)DDA,刚才说过了BC的画图函数,其中有个line,在众多的程序工具中,往往提供了画常用图形的函数,比如刚才的line,但就像以前使用的数学定理一样,有的时候不仅知道怎么用,还要知道怎么证出来的,比如,上边这个直线函数,到底是怎么把直线画出来的,也就是说,咱们要自己写一个画线函数。
这个时候,需要解决的是两个问题
1:以什么为单位来画,显然,直线的单位就是点,点动成线
2:以什么方法通过画点来画出直线
DDA算法就是解决第二个问题的,这个的原理简单说一下,直线的方程咱们最常用的是两种方式:斜截式和点斜式,DDA是斜截式的应用,比如,某个点已定(x0,y0),从这个点开始,向前多一个像素(x=x+1)就有(y=y+k),于是,算法就有了,用一个循环,x从起点横坐标开始,终点横坐标结束,然后每次x++,y都+k,得到的点用画点函数画出来。
这样吧,算法说过了,再补充说一下这个算法的详细数学推导过程:
令直线方程为y=kx+b,起点(x0,y0),下一个点,x=x0+dx,dx表示横坐标的增量,如果以像素步进,显然,dx=1,好,对于下一个y,y=k(x0+dx)+b,整理一下,y=kx0+b+kdx,当然,kx0+b=y0,而像素步进的时候,dx=1,所以就变成了y=y+k。
不过不取1,也可以画出些好玩的东西,就可以做出自定义的画线函数,大家可以试试。
#include <graphics.h>
#include <math.h>
#define ROUND(a) ((int)(a+0.5))
#define OX 320
#define OY 240
void lineDDA (int xa, int ya, int xb, int yb, int color);
void setpixel (int x, int y, int color);
main(){
int gdrive=DETECT, gmode=0;
initgraph(&gdrive, &gmode, "d://tc");
setbkcolor(BLACK);
line (0, OY, 2*OX, OY);
line (OX, 0, OX, 2*OY);
lineDDA (100, 10, 0, 0, RED);
getch ();
closegraph();
return 0;
}
void lineDDA (int xa, int ya, int xb, int yb, int color) {
int dx = xb - xa;
int dy = yb - ya;
int steps, i;
float xIncrement, yIncrement;
float x=xa;
float y=ya;
if(abs(dx) > abs(dy))
steps = abs(dx);
else
steps = abs(dy);
/*
* y=kx+b, if k>0, x+1 and y+k; if k<0, y+1 and x+1/k.
*/
xIncrement = dx/(float)steps;
yIncrement = dy/(float)steps;
putpixel (ROUND(x), ROUND(y), color);
for (i=0; i<steps; i++) {
x += xIncrement;
y += yIncrement;
setpixel (ROUND(x), ROUND(y), color);
}
return;
}
void setpixel (int x, int y, int color) {
/* printf ("(%d,%d)", x, y);
*/ putpixel (OX+x, OY-y, color);
return;
}
这个算法的一个主要缺陷,就是当斜率较大的时候会出现比较明显的锯锯齿
(二)Cohen-Sutherland算法
Sutherland,全名Ivan Sutherland,号称图形学的祖师,这位老爹的毕业设计据说是自己做了一部电影,这次要说的,是个比较简单的算法,是关于直线裁剪的,当然,就像Newton第二定理虽然看起来很简单,但也不能认为Newton就那么两下子一样,因为在图形学的算法中,以这位老爹命名和他参与的算法可是随处可见,Cohen—Sutherland这个算法的应用就是直线裁剪,直线裁剪的意思就是当直线用过某个多边形的时候,把直线在多边形内的部分特殊表示,当然,其中也要加上直线不通过这个多边形的判断。
先把多边形看成一个窗口,对于某个线段和某个多边形的关系,可以这么看,线段和这个窗口的关系是有三种情况的:
1:线段完全在窗口中
2:线段完全在窗口外
3:以上两者之外的情况
前两种情况,程序处理起来较为容易,第三种情况,需要特别处理。
先用方程求出直线和窗口边界的交点,然后以交点为界,对线段进行分割,在窗口外的可以不予理会,另外一段进入循环,继续判断到底有多少在窗口内,一般常用的判断方法,是抽象出一个小的矩形窗口,然后判断,为了能快速判断线段的某一段和窗口的关系,采用了编码方法,比如,一个窗口周围,以直线划分,可出现八个区域,加上窗口自身,一共九个区域,对这八个区域,咱们先给编上码,最上边那一行“1001,1000,
1001,1000,1010
0001,0000,0010
0101,0100,0110
每个区域四位,就是用这个区域的纵坐标和Ymax,Ymin,Xmax,Xmin比较,根据大于小于的关系,转为“0、1”,有了这组区域编码,剩下的问题就好办了,首先,算出线段两个端点所在区域,得到两个值,code1和code2,因为之前是用0,1进行组合,所以这个时候,有关的判断显然应该使用C语言的位运算,若两个编码的位与运算得出非0,说明线段的两个端点必然同在窗口的某一侧,直接问题解决,否则,就分别对四个边界的值进行x和y的交点求解。
使用while循环,循环的条件是这样的:
while(code1!=0 || code2!=0 )
if((intCode1 & intCode2)!=0)
return;
这就说明是直线和窗口毫无关系,直接返回。
完整的循环应该是这样的
while(intCode1!=0||intCode2!=0)
{
if((intCode1 & intCode2)!=0)
return;
intCode=intCode1;
if(intCode1==0)
{
intCode=intCode2;
}
if((Left & intCode)!=0)
{
x=Xl;
y=ys+(ye-ys)*(Xl-xs)/(xe-xs);
}
else if((Right & intCode)!=0)
{
x=Xr;
y=ys+(ye-ys)*(Xr-xs)/(xe-xs);
}
else if((Bottom & intCode)!=0)
{
y=Yb;
x=xs+(xe-xs)*(Yb-ys)/(ye-ys);
}
else if((Top & intCode)!=0)
{
y=Yt;
x=xs+(xe-xs)*(Yt-ys)/(ye-ys);
}
if(intCode==intCode1)
{
xs=x;
ys=y;
intCode1=cohenCode(x,y);
}
else
{
xe=x;
ye=y;
intCode2=cohenCode(x,y);
}
}
下面我们再看一下:
if((intCode1 & intCode2)!=0)
return;
intCode=intCode1;
这个算法在网上和书上都可以看到,但好多都有错误,比如上边那句,不少书都会忘记一个赋值语句。
intCode=intCode1;
如果少了这句,就会直接缺一种情况,就是直线和矩形只有一个交点的时候,会发生反折现象。
x1=xs;
y1=ys;
x2=xe;
y2=ye;
还有在while循环的最后应该加上这样的一个过程
其中,xs,ys,xe,ye都是开始的点,经过循环处理之后,应该把这些值赋给画图准备点,就是前边的下标为1的。
这两处的遗漏在图形学的书上随处可见,由此也可以大概猜出咱们的好多图形学的书都是怎么出的,把上述的一些过程进行整理,为了更好的读,把功能划分为三个函数:
void proAssign(float xs,float ys,float xe,float ye)
这个是专门用来把处理好的数赋值给画线坐标
int cohenCode(float x,float y)
{
int c=0;
if(x<Xl)
c|=Left;
if(x>Xr)
c|=Right;
if(y<Yb)
c|=Bottom;
if(y>Yt)
c|=Top;
return c;
}
这个函数是用来生成区域的编码
void cohenProcess(float xs,float ys,float xe,float ye)
{
int intCode1,intCode2,intCode;
float x,y;
intCode1=cohenCode(xs,ys);
intCode2=cohenCode(xe,ye);
while(intCode1!=0||intCode2!=0)
{
if((intCode1 & intCode2)!=0)
return;
intCode=intCode1;
if(intCode1==0)
{
intCode=intCode2;
}
if((Left & intCode)!=0)
{
x=Xl;
y=ys+(ye-ys)*(Xl-xs)/(xe-xs);
}
else if((Right & intCode)!=0)
{
x=Xr;
y=ys+(ye-ys)*(Xr-xs)/(xe-xs);
}
else if((Bottom & intCode)!=0)
{
y=Yb;
x=xs+(xe-xs)*(Yb-ys)/(ye-ys);
}
else if((Top & intCode)!=0)
{
y=Yt;
x=xs+(xe-xs)*(Yt-ys)/(ye-ys);
}
if(intCode==intCode1)
{
xs=x;
ys=y;
intCode1=cohenCode(x,y);
}
else
{
xe=x;
ye=y;
intCode2=cohenCode(x,y);
}
}
proAssign(xs,ys,xe,ye); //用处理完毕的xs,ys,xe,ye对x1,y1,x2,y2进行赋值
}
下边的是完整的程序
//Author:9Cat
//OS:WindowsXP
//Complier:Borland C++ 3.1
//CurrentVersion:1.0
//Function:Cohen-Sutherland line cut
//First Create Time:2006-9-23 14:00
//Last Modify Time:2006-9-24 02:00
#include <stdio.h>
#include <graphics.h>
//预定义初始线段两端点
#define Xs 20
#define Ys 20
#define Xe 400
#define Ye 300
//预定义初始矩形两顶点
#define Xl 40
#define Yb 60
#define Xr 320
#define Yt 200
//设置初始边界变量
#define Left 1
#define Right 2
#define Bottom 4
#define Top 8
//线段的两端点
float x1,y1,x2,y2;
//赋值函数声明,用于为全局变量x1,y1,x2,y2赋值
void proAssign(float,float,float,float);
//Cohen-Sutherland编码函数声明
int cohenCode(float,float);
//Cohen-Sutherland处理函数声明
void cohenProcess(float,float,float,float);
void main()
{
registerbgidriver(EGAVGA_driver); //注册图形驱动
int graphdriver=DETECT,graphmode;
initgraph(&graphdriver,&graphmode,""); //初始化图形环境
cleardevice(); //清空屏幕
proAssign(Xs,Ys,Xe,Ye); //为初始线段绘制赋值
setcolor(GREEN); //设定初始线段绘图颜色
line(x1,y1,x2,y2); //绘制待裁剪线段
rectangle(Xl,Yb,Xr,Yt); //绘制矩形
cohenProcess(x1,y1,x2,y2); //Cohen-Sutherland算法处理
setcolor(RED); //设置裁剪得出线段颜色
setlinestyle(0,0,3); //设置裁剪线段线型
line(x1,y1,x2,y2); //绘制裁剪后线段
getchar();
}
//赋值函数定义
void proAssign(float xs,float ys,float xe,float ye)
{
x1=xs;
y1=ys;
x2=xe;
y2=ye;
}
//Cohen编码函数定义
int cohenCode(float x,float y)
{
int c=0;
if(x<Xl)
c|=Left;
if(x>Xr)
c|=Right;
if(y<Yb)
c|=Bottom;
if(y>Yt)
c|=Top;
return c;
}
//Cohen处理函数定义,具体细节请参考有关图形学文章
void cohenProcess(float xs,float ys,float xe,float ye)
{
int intCode1,intCode2,intCode;
float x,y;
intCode1=cohenCode(xs,ys);
intCode2=cohenCode(xe,ye);
while(intCode1!=0||intCode2!=0)
{
if((intCode1 & intCode2)!=0)
return;
intCode=intCode1;
if(intCode1==0)
{
intCode=intCode2;
}
if((Left & intCode)!=0)
{
x=Xl;
y=ys+(ye-ys)*(Xl-xs)/(xe-xs);
}
else if((Right & intCode)!=0)
{
x=Xr;
y=ys+(ye-ys)*(Xr-xs)/(xe-xs);
}
else if((Bottom & intCode)!=0)
{
y=Yb;
x=xs+(xe-xs)*(Yb-ys)/(ye-ys);
}
else if((Top & intCode)!=0)
{
y=Yt;
x=xs+(xe-xs)*(Yt-ys)/(ye-ys);
}
if(intCode==intCode1)
{
xs=x;
ys=y;
intCode1=cohenCode(x,y);
}
else
{
xe=x;
ye=y;
intCode2=cohenCode(x,y);
}
}
proAssign(xs,ys,xe,ye); //用处理完毕的xs,ys,xe,ye对x1,y1,x2,y2进行赋值
}
这就是最终完整的程序
这个是BC版的,BC3.1版的这个程序可能在网上也很难找到,因为这个复杂的算法很少有人会用BC去做的,大部分都是OpenGL的,改天我会再次实现一个OpenGL版,发上来大家对比一下。
本文来源:某程序设计群群主-----九命猫
文稿整理者:同一群内群员-------太阳
最终定稿:九命猫
感谢“太阳”的整理工作。