【win32】计算机图形学——中点法画线和八分法画圆

在《计算机图形学基础教程》中,对于中点法画线和八分法画圆的描述,一如《数据结构》使用了伪代码,而且这门课在本科里面基本上是不会有必修课地位的,同时,计算机图形学里面的代码实现并不像写个控制台程序写写就完事,起码,我觉得一个win32应用程序编程基础都没有人,要将这段代码转化为一个可以使用的代码是难于登天的。

计算机图形学对于进军计算机游戏制作有着重要的意义。对此进行一番研究也是好的。

win32程序里面画线、画图和画矩形是极其简单,都有封装好的方法,一行Rectagle()、Ellipse()等等就完了,下面做这个画图程序,主要是说明如果利用计算机图形学的原生代码,中点法画线和八分法画圆,为了更有趣一点,还提供的颜色选择,以此说明一个普通的win32的制作过程。


1、如同《【win32】vs2010的窗体程序Helloworld》(点击打开链接)新建完一个win32程序之后,我们先要对程序进行一些小修改,诸如程序栏标题、菜单,删去最大最小化按钮以避免重绘的修改和删除对话框。

首先在Resource.h之中我新增定义了如菜单项:

//自己定义的菜单项
#define IDM_LINE 201
#define IDM_CIRCLE 202
#define IDM_BLACK 301
#define IDM_RED 302
#define IDM_GREEN 303
#define IDM_BLUE 304
如下图:


然后此程序被我命名成Painter,在Painter.rc之中将“关于”对话框和Alt+?、/快捷键删除,并对菜单项修改如下代码,具体怎么删除在《【win32】标记菜单与对话框背景色》(点击打开链接)说过了,这里不再赘述。

	//
	// 菜单
	//

	IDC_PAINTER MENU
	BEGIN
	POPUP "图形(&I)"
	BEGIN
	MENUITEM "中点法画线(&L)",                IDM_LINE
	MENUITEM "八分法画圆(&C)",                IDM_CIRCLE
	END
	POPUP "颜色(&C)"
	BEGIN
	MENUITEM "黑(&B)",           IDM_BLACK
	MENUITEM "红(&R)",           IDM_RED
	MENUITEM "绿(&G)",           IDM_GREEN
	MENUITEM "蓝(&E)",           IDM_BLUE
	END
	END
同时将“字符串表”中的IDS_APP_TITLE改成"计算机图形学画图",也就是此程序的标题栏名字,如下图所示:


2、之后就真的可以在Painter.h和Painter.cpp进行主要代码的编写了。

(1)首先是在Painter.h引入一个#include "math.h",一会儿求两点距离开平方的时候要用到这个头文件,如下图所示:


(2)对于Painter.cpp中的BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)程序初始化函数定义两个全局变量,用于记录用户选择的计算机图形学算法和颜色,并修改如下:

int algorithm;//记录用户当前选择的算法与使用的颜色
COLORREF color;//画笔颜色
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	HWND hWnd;

	hInst = hInstance; // 将实例句柄存储在全局变量中

	hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX^WS_MINIMIZEBOX,
		0, 0, 640, 480, NULL, NULL, hInstance, NULL);//调整窗口大小、样式,640*480的大小,在屏幕左上角打开,同时取出最大最小化按钮避免重绘

	if (!hWnd)
	{
		return FALSE;
	}

	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);

	auto menu = GetMenu(hWnd);//获取菜单栏的句柄
	auto sub_menu0 = GetSubMenu(menu, 0);//获取第一个弹出菜单,即[图形]菜单的句柄
	auto sub_menu1 = GetSubMenu(menu, 1);//获取第二个弹出菜单,即[颜色]菜单的句柄
	/*默认是画线和黑色*/
	CheckMenuRadioItem(sub_menu0, IDM_LINE, IDM_CIRCLE, IDM_LINE, MF_BYCOMMAND);			
	//令此菜单单选,第一个参数是菜单句柄、第二、三个参数是指明这个菜单从哪个编号到哪个编号是单选的,第四个参数是选择谁
	algorithm=IDM_LINE;
	CheckMenuRadioItem(sub_menu1, IDM_BLACK, IDM_BLUE, IDM_BLACK, MF_BYCOMMAND);
	color=RGB(0,0,0);

	return TRUE;
}
对于默认创建的窗口,可以在hWnd中进行修改,里面的第3个参数确定了窗口的样式,通过异或运算^可以增加样式,这里WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX^WS_MINIMIZEBOX中,WS_OVERLAPPEDWINDOW是默认样式,WS_THICKFRAME是禁止调整窗口的大小,WS_MAXIMIZEBOX^WS_MINIMIZEBOX意为去除最大最小化按钮。

之后的第4第5个参数是指窗口创建的位置,坐标(0,0)就是屏幕的最左上角。

而第6第7个参数,就是窗口的大小,这里定义为640*480。

接下的定义默认的菜单选择和初始化两个变量。

(3)之后将消息回调函数LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam),先定义2个全局变量,用于记录鼠标的位置,并修改成如下:

POINT pointdown,pointup;//是一个结构体,储存鼠标x,y坐标
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;

	auto menu = GetMenu(hWnd);//获取菜单栏的句柄
	auto sub_menu0 = GetSubMenu(menu, 0);//获取第一个弹出菜单,即[图形]菜单的句柄
	auto sub_menu1 = GetSubMenu(menu, 1);//获取第二个弹出菜单,即[颜色]菜单的句柄

	switch (message)
	{
	case WM_COMMAND:  
		wmId    = LOWORD(wParam);  
		wmEvent = HIWORD(wParam); 
		switch (wmId)//分析窗口的选择  
		{ 
		case IDM_LINE:
			CheckMenuRadioItem(sub_menu0, IDM_LINE, IDM_CIRCLE, IDM_LINE, MF_BYCOMMAND);			
			//令此菜单单选,第一个参数是菜单句柄、第二、三个参数是指明这个菜单从哪个编号到哪个编号是单选的,第四个参数是选择谁
			algorithm=IDM_LINE;
			break; 
		case IDM_CIRCLE: 
			CheckMenuRadioItem(sub_menu0, IDM_LINE, IDM_CIRCLE, IDM_CIRCLE, MF_BYCOMMAND);
			algorithm=IDM_CIRCLE;
			break; 
		case IDM_BLACK:
			CheckMenuRadioItem(sub_menu1, IDM_BLACK, IDM_BLUE, IDM_BLACK, MF_BYCOMMAND);
			color=RGB(0,0,0);
			break;   
		case IDM_RED:  
			CheckMenuRadioItem(sub_menu1, IDM_BLACK, IDM_BLUE, IDM_BLACK, MF_BYCOMMAND);
			color=RGB(255,0,0);
			break;    
		case IDM_GREEN:  
			CheckMenuRadioItem(sub_menu1, IDM_BLACK, IDM_BLUE, IDM_GREEN, MF_BYCOMMAND);
			color=RGB(0,255,0);
			break;  
		case IDM_BLUE:   
			CheckMenuRadioItem(sub_menu1, IDM_BLACK, IDM_BLUE, IDM_GREEN, MF_BYCOMMAND);
			color=RGB(0,0,255);
			break;  
		}  
		break;
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		// TODO: 在此添加任意绘图代码...
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;

	case WM_LBUTTONDOWN:
		{
			pointdown.x= LOWORD(lParam);//记录按下鼠标坐标信息
			pointdown.y= HIWORD(lParam);
		}
		break;
	case WM_LBUTTONUP:
		{
			pointup.x= LOWORD(lParam);//记录松开鼠标坐标信息
			pointup.y= HIWORD(lParam);
			HDC hdc=GetDC(hWnd);  //HDC是Windows的设备描述表句柄,此处指对话框的客户区
			switch(algorithm){
			case IDM_LINE:{//必须有这个括号,不然会提示变量不能在case中初始化的错误
				int x1=pointdown.x, x2=pointup.x;
				int y1=pointdown.y, y2=pointup.y;
				float k=1.0*(y2-y1)/(x2-x1); //斜率  
				int flag=0;//是否沿y=x翻转  
				if (k>1||k<-1){//根据要斜率,判断直线是否超过y=x或者y=-x这条直线,是以x为底基还是以y为底基
					flag=1;  
					x1^=y1^=x1^=y1;
					x2^=y2^=x2^=y2;  
					k=1.0*(y2-y1)/(x2-x1);  
				}  
				float d=0.5-k;//初始值  
				if (x1>x2){  
					x1^=x2^=x1^=x2;  
					y1^=y2^=y1^=y2;  
				}  
				while(x1!=x2){//主位移,每次都像素+1  
					if(k>0&&d<0){//正向  
						++y1,++d;  
					}
					else if(k<0&&d>0){//负向  
						--y1,--d; 
					}
					d-=k;  
					++x1;  
					if(flag){
						SetPixel(hdc,y1,x1,color);//翻转像素点  
					}
					else{ 
						SetPixel(hdc,x1,y1,color); 
					}  
				} 
				break;
						  }
			case IDM_CIRCLE:{
				int ox=(pointdown.x+pointup.x)/2,oy=(pointdown.y+pointup.y)/2;//根据用户两次按下鼠标的坐标,求圆的重点
				float r=sqrt(((pointdown.x-pointup.x)*(pointdown.x-pointup.x)+(pointdown.y-pointup.y)*(pointdown.y-pointup.y))*1.0)/2;
				//根据用户两次按下鼠标的坐标,两点距离公式/2求半径,其中最后的*1.0是强制将这个参数转成开平方根sqrt()函数只接受的double型
				//以下是八分法画圆
				float d=1.25-r;  
				int x=0,y=r,fx=r/1.4;  
				while (x!=fx){  
					if(d<0){
						d+=2*x+3;
					}
					else{  
						d+=2*(x-y)+5;  
						--y;  
					}  
					SetPixel(hdc,ox+x,oy+y,color);  
					SetPixel(hdc,ox+x,oy-y,color);  
					SetPixel(hdc,ox-x,oy+y,color);  
					SetPixel(hdc,ox-x,oy-y,color);  
					SetPixel(hdc,ox+y,oy-x,color);  
					SetPixel(hdc,ox+y,oy+x,color);  
					SetPixel(hdc,ox-y,oy+x,color);  
					SetPixel(hdc,ox-y,oy-x,color);  
					++x;  
				}  
				break;
							}

			}
			ReleaseDC(hWnd,hdc);//释放句柄
		}
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

至此,整个程序就这样完成了,下面对几个要点进行说明。

之所以在这个消息回调函数之外定义记录鼠标按下放起位置的变量,主要是因为消息回调函数,每时每刻的在执行,在轮询用户对这个窗口做的操作,如果将这个变量定义到这个窗口以内,则会时刻被初始化,导致永远难以记录到鼠标位置,不合理。

由于我们禁止了调整窗口大小和最大最小化,所以重绘事件case WM_PAINT可以不写任何代码不考虑。

全文的精粹在于鼠标松开时,立即进行中点法画线和八分法画圆的两个计算机图形学的核心算法。

一、中点法画线

比起贝塞尔直线,此算法不用算乘法和除法,过各行各列象素中心构造一组虚拟网格线。按直线从起点到终点的顺序计算直线与各垂直网格线的交点,然后根据误差项的符号确定该列象素中与此交点最近的象素。


设直线方程为:,其中k=dy/dx。 因为直线的起始点在象素中心,所以误差项d的初值d0=0。
X下标每增加1,d的值相应递增直线的斜率值k,即d=d+k。一旦d≥1,就把它减去1,这样保证d在0、1之间。当d≥0.5时,最接近于当前象素的右上方象素(Xi+1,Yi+1),而当d<0.5时,更接近于右方象素(Xi+1,Yi)。为方便计算,令e=d-0.5,e的初值为-0.5,增量为k。当e≥0时,取当前象素(xi,yi)的右上方象素(Xi+1,Yi+1)。而当e<0时,更接近于右方象素(Xi+1,Yi+1)。可以改用整数以避免除法。由于算法中只用到误差项的符号,因此可作如下替换: 

例如:Line: P0(0, 0),P1(5,2)。则k=dy/dx=0.4。由于此直线斜率少于1,也就是在y=x直线的右下方,那么则以x作为主偏移,每个新点的横坐标x都会自增。


至于纵坐标y是否自增则按照上述公式判断,具体如下:


那么我们就能用6个点在计算机表示这条在理论上是由无限点构成的直线,而且肉眼是看不出区别。

至于矩形等其它多边形,可以用这个算法进一步延伸。

二、八分法画圆

整个方法和中点法画线类似。整体思想是要用尽可能少的点在计算机表达出肉眼和平常一样看不出差别的圆。

圆的特征是八对称性。只要扫描转换八分之一圆弧,就可以求出整个圆弧的象素集。考虑中心在原点,半径为R的第二个八分圆。

构造判别式(圆方程):


若d<0,则取P1为下一像素,而且再下一像素的判别式为:

若d>=0, 则应取P2为下一像素,而且下一像素的判别式为:

第一个像素是(0,R),判别式d的初始值为:

所以,据此,便有了以上的代码。

发布了750 篇原创文章 · 获赞 956 · 访问量 368万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览