光栅图形学(一)——直线段的扫描转换算法

版权声明:欢迎转载和交流。 https://blog.csdn.net/Hachi_Lin/article/details/88092964

1. 数值微分法

  已知过端点 P 0 ( x 0 , y 0 ) P_0(x_0,y_0) P 1 ( x 1 , y 1 ) P_1(x_1,y_1) 的直线段 L ( P 0 , P 1 ) L(P_0,P_1) ;直线斜率 k = y 1 y 0 x 1 x 0 k = \frac{y_1 - y_0}{x_1 - x_0} 。画线的过程为:从 x x 的左端点 x 0 x_0 开始,向右端点步进,步长为1(像素),按 y = k x + b y = kx +b 计算相应的 y y 坐标,并取像素点 ( x , r o u n d ( y ) ) (x,round(y)) 作为当前坐标。但这样做,计算每一个点需要做一个乘法、一个加法。设步长为 Δ x \Delta x ,有 x i + 1 = x i + Δ x x_{i+1} = x_i + \Delta x ,于是
(1.1) y i + 1 = k x i + 1 + b = k x i + k Δ x + b = y i + k Δ x y_{i+1} = kx_{i+1} + b = kx_i + k \Delta x + b = y_i + k\Delta x \tag{1.1}
  当 Δ x = 1 \Delta x = 1 时,有 y i + 1 = y i + k y_{i+1} = y_i + k 。即 x x 每递增1, y y 递增 k k (即直线的斜率)。这样,计算就由一个乘法和加法减少为一个加法。

  • 算法程序:DDA画线算法程序
void DDALine(int x0, int y0, int x1, int y1, int color)
{
	int x;
	float dx, dy, y, k;
	dx = x1 - x0, dy = y1 - y0;
	k = dy/dx, y = y0;
	for(x = x0; x < x1; x++,y+=k)
	{
		drawpiexl(x, int(y + 0.5), color);
	}
}

注意: 用int(y + 0.5)取整的目的是为取离真正交单近的像素网格点作为光栅化的点。
  应当注意的是,上述算法仅适用于 k 1 k \leq 1 的情形。在这种情况下, x x 每增加1, y y 最多增加1。当 k 1 |k| \ge 1 时,必须把 x , y x,y 地位互换, y y 每增加1, x x 相应增加 1 / k 1/k 。本节的其它算法也是如此。在这个算法中, y y k k 必须用浮点数表示,而且每一步都要对 y y 进行四舍五入后取整,这使得该算法不利于硬件的实现。

2.中点画线法

在这里插入图片描述
  通过观察可以发现,画直线段的过程中,当前像素点 ( x p , y p ) (x_p,y_p) ,下一个像素点有两种选择点 P 1 ( x p + 1 , y p ) P_1(x_p+1,y_p) P 1 ( x p + 1 , y p + 1 ) P_1(x_p+1,y_p+1) 。若 M = ( x p + 1 , y p + 0.5 ) M = (x_p+1,y_p+0.5) P 1 P_1 P 2 P_2 的中点, Q Q 为理想直线与 x = x p + 1 x = x_p +1 垂线的交点。当 M M Q Q 的下方时, P 2 P_2 应为下一个像素点;当 M M Q Q 的上方时,应取 P 1 P_1 为下一点。这就是中点画线法的基本原理(可见下图)。对直线段 L ( p 0 ( x 0 , y 0 ) , p 1 ( x 1 , y 1 ) ) ) L(p_0(x_0,y_0),p_1(x_1,y_1))) ,采用方程 F ( x , y ) = a x + b x + c = 0 F(x,y) = ax + bx +c = 0 表示,其中 a = y 0 y 1 , b = x 1 x 0 , c = x 0 y 1 x 1 y 0 a = y_0 - y_1,b = x_1 - x_0,c = x_0y_1 - x_1y_0 。于是有下述点与 L L 的关系:
{ 线上 F ( x , y ) = 0 上方 F ( x , y ) &gt; 0 下方 F ( x , y ) &lt; 0 \begin{cases} \text{线上}:F(x,y) = 0 \\ \text{上方}:F(x,y) &gt; 0 \\ \text{下方}:F(x,y) &lt; 0 \\ \end{cases}
  因此,欲判断 M M Q Q 上方还是下方,只要把 M M 代入 F ( x , y ) F(x,y) ,并判断它的符号即可。构造如下判别式:
(1.2) d = F ( M ) = F ( x p + 1 , y p + 0.5 ) = a ( x p + 1 ) + b ( y p + 0.5 ) + c d = F(M) = F(x_p+1,y_p+0.5) = a(x_p+1) + b(y_p+0.5)+c \tag{1.2}
  当 d &lt; 0 d &lt; 0 M M L L ( Q Q 点)下方,取 P 2 P_2 为下一像素;当 d &gt; 0 d &gt; 0 M M L L ( Q Q 点)上方,取 P 1 P_1 为下一像素;当 d = 0 d = 0 ,选 P 1 P_1 P 2 P_2 均可,约定取 P 1 P_1 为下一个像素。
  用式(1.2)计算符号时,需要4个加法和2个乘法。事实上, d d x p , y p x_p,y_p 的线性函数,因此可采用增量计算,提高运算效率。方法如下

  • (1)若 d 0 d \ge 0 ,则取正右方像素 P 1 ( x p + 1 , y p ) P_1(x_p +1,y_p) 。判断下一个像素的位置时,应计算 d 1 = F ( x p + 2 , y p + 0.5 ) = a ( x p + 2 ) + b ( y p + 0.5 ) + c = d + a d_1 = F(x_p+2,y_p+0.5)= a(x_p+2)+b(y_p+0.5)+c = d + a ,增量为 a a
  • (2)若 d &lt; 0 d &lt; 0 ,则取正右方像素 P 2 ( x p + 1 , y p + 1 ) P_2(x_p +1,y_p+1) 。判断下一个像素的位置时,应计算 d 1 = F ( x p + 2 , y p + 1.5 ) = a ( x p + 2 ) + b ( y p + 1.5 ) + c = d + a + b d_1 = F(x_p+2,y_p+1.5)= a(x_p+2)+b(y_p+1.5)+c = d + a + b ,增量为 a + b a+b
      设从点 ( x 0 , y 0 ) (x_0,y_0) 开始画线, d d 的初值 d 0 = F ( x p + 1 , y p + 0.5 ) = F ( x 0 , y 0 ) + a + 0.5 b d_0 = F(x_p+1,y_p+0.5) = F(x_0,y_0)+a+0.5b 。因 F ( x 0 , y 0 ) = 0 F(x_0,y_0)=0 ,则 d 0 = a + 0.5 b d_0 = a + 0.5b 。由于使用的只是 d d 的符号,而 d d 的增量都是整数,只是初始值包含小数。因此可以用 2 d 2d 代替 d d 来摆脱点运算,写出仅包含整数运算的算法。
  • 算法程序:中点画线算法程序
void MidpointLine(int x0, int y0, int x1, int y1, int color)
{
	int a, b, d1, d2, d, x, y;
	a = y0 - y1, b = x1 - x0, d = 2*a + b;
	d1 = 2*a, d2 = 2*(a + b);
	x = x0, y = y0;
	drawpixel(x,y,color);
	while(x < x1)
	{
		if(d < 0)
			x++, y++, d+=d2;
		else
			x++, d+=d2;
		drawpixel(x,y,color)}
}

3.Bresenham算法

  Bresenham算法是在计算机图形学领域内使用最广泛的直线扫描转换算法。该方法类似于中点法,由误差项符号决定下一个像素取右边点还是右上角。
  算法原理如下:过各行各列像素中心构造一组虚拟网络线,按直线从起点到终点的顺序计算直线与各垂直网络的交点,然后确定该列像素中与此交点最近的像素。该算法的巧妙在于采用增量计算,使得每一列,只要检查一个误差项的符号,就可以确定该列所求的像素。
  如图所示,设直线方程为 y = k x + b y=kx +b ,则有 y i + 1 = y i + k ( x i + 1 x i ) = y i + k y_{i+1} = y_i +k(x_{i+1}-x_i) = y_i +k ,其中 k k 为直线的斜率。假设 x x 列的像素坐标已经确定为 x i x_i ,其行坐标 y i y_i ,那么下一个像素的列坐标为 x i + 1 x_i + 1 ,而行坐标要么为 y i y_i 要么为 y i + 1 y_i +1 。是否增1取决于图所示的误差项 d d 的值。因为直线的起始点在像素中心,所以误差项 d 0 = 0 d_0 = 0 x x 下标每增加1, d d 的值相应递增直线斜率值 k k ,即 d = d + k d = d+k 。当 d 0.5 d \ge 0.5 时,直线与 x i + 1 x_i +1 列垂直网络的交点最接近于当前像素 ( x i , y i ) (x_i,y_i) 的右上方像素 ( x i + 1 , y i + 1 ) (x_i+1,y_i+1) ,该像素在 y y 方向增加1,同时作为下一次计算的新基点,因此 d d 值相应减1;而当 d &lt; 0.5 d &lt; 0.5 ,更接近于右方像素 ( x i + 1 , y i ) (x_i+1,y_i) 。为了方便计算,令 e = d 0.5 e = d - 0.5 e e 的初始值为-0.5,增量为k。当 e 0 e \ge 0 ,取当前像素 ( x i , y i ) (x_i,y_i) 的右上方像素 ( x i + 1 , y i + 1 ) (x_i+1,y_i+1) e e 减小1;而当 e &lt; 0 e &lt; 0 时,更接近于右方的像素 ( x i + 1 , y i ) (x_i+1,y_i)
在这里插入图片描述

  • Bresenham画线算法程序
void BresenhamLine(int x0, int y0, int x1, int y1, int color)
{
	int x, y, dx, dy;
	float k, e;
	dx = x1 - x0, dy = y1 - y0, k = dy/dx;
	e = -0.5, x = x0, y = y0;
	for(int i = 0; i < dx; i++)
	{
		drawpixel(x, y, color);
		x = x + 1, e = e + k;
		if(e >= 0)
			y++, e = e-1;
	}
}

上述Bresenham算法在计算直线斜率与误差项时用到了小数与除法,可以改用整数以避免除法。由于算法中只用到了误差项的符号,因此可以做如下替换 e = 2 e d x e&#x27; = 2*e *dx

  • 改进的Bresenham画线算法程序
void BresenhamLine(int x0, int y0, int x1, int y1, int color)
{
	int x, y, dx, dy, e;
	dx = x1 - x0, dy = y1 - y0, e = -dx;
	x = x0, y = y0;
	for(int i = 0; i < dx; i++)
	{
		drawpixel(x, y, color);
		x = x + 1, e = e + 2*dy;
		if(e >= 0)
			y++, e = e-2*dx;
	}
}

4. 代码实现(VS2017 + OpenGL)

#include "pch.h"
#include <iostream>
#include <gl\glut.h>
#include <gl\GL.h>
#include <gl\GLU.h>
using namespace std;

void init(void)
{
	glClearColor(1.0, 1.0, 1.0, 0.0);  // Set display-window color to white.
	glMatrixMode(GL_PROJECTION);       // Set projection parameters.
	gluOrtho2D(0.0, 200.0, 0.0, 150.0);
}


//窗口大小改变时调用的登记函数
void ChangeSize(GLsizei w, GLsizei h) {
	if (h == 0)
		h = 1;
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	if (w <= h)
		glOrtho(0.0f, 250.0f, 0.0f, 250.0f*h / w, 1.0, -1.0);
	else
		glOrtho(0.0f, 250.0f*w / h, 0.0f, 250.0f, 1.0, -1.0);
}

//DDA算法函数
void DDALine(int x0, int y0, int x1, int y1)
{
	glColor3f(1.0, 0.0, 0.0);  //设置像素点颜色为红色
	glPointSize(2.0f);         //设置像素点大小
	int x;
	float dx, dy, y, k;
	dx = x1 - x0, dy = y1 - y0;
	k = dy / dx, y = y0;
	for (x = x0; x < x1; x++, y += k)
	{
		glVertex2i(x, int(y + 0.5));
	}
}
//中点画线算法
void MidpointLine(int x0, int y0, int x1, int y1)
{
	glColor3f(0.0, 1.0, 0.0);  //设置像素点颜色为绿色
	glPointSize(2.0f);         //设置像素点大小
	int a, b, d1, d2, d, x, y;
	a = y0 - y1, b = x1 - x0, d = 2 * a + b;
	d1 = 2 * a, d2 = 2 * (a + b);
	x = x0, y = y0;
	glVertex2f(x, y);
	while (x < x1)
	{
		if (d < 0) {
			x++, y++, d += d2;
		}
		else {
			x++, d += d1;
		}
		glVertex2f(x, y);
	}
}
//Bresenham算法画线
void BresenhamLine(int x0, int y0, int x1, int y1)
{
	glColor3f(0.0, 0.0, 1.0);  //设置像素点颜色为蓝色
	glPointSize(2.0f);         //设置像素点大小
	int x, y, dx, dy, e;
	dx = x1 - x0, dy = y1 - y0, e = -dx;
	x = x0, y = y0;
	for (int i = 0; i < dx; i++)
	{
		glVertex2f(x, y);
		x = x + 1, e = e + 2 * dy;
		if (e >= 0)
			y++, e = e - 2 * dx;
	}
}
void display()
{
	int x1_1 = 20, y1_1 = 20, x2_1 = 160, y2_1 = 80;
	int x1_2 = 20, y1_2 = 40, x2_2 = 160, y2_2 = 100;
	int x1_3 = 20, y1_3 = 60, x2_3 = 160, y2_3 = 120;
	glClear(GL_COLOR_BUFFER_BIT);
	glBegin(GL_POINTS);
	DDALine(x1_1, y1_1, x2_1, y2_1);
	glEnd();
	glBegin(GL_POINTS);
	MidpointLine(x1_2, y1_2, x2_2, y2_2);
	glEnd();
	glBegin(GL_POINTS);
	BresenhamLine(x1_3, y1_3, x2_3, y2_3);
	glEnd();
	glFlush();
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv); //GLUT初始化
	glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); //指出显示窗口使用单个缓存且使用由红绿蓝三种颜色模型
	glutInitWindowPosition(500, 500);            //设置显示窗口位置
	glutInitWindowSize(400, 400);                //设置显示窗口大小
	glutCreateWindow("Line Algorithm");           //设置显示窗口的标题
	glutDisplayFunc(display);                    //将图赋值给显示窗口
	glutReshapeFunc(ChangeSize);
	init();
	glutMainLoop();              //必须是程序的最后一个
	return 0;
}
  • 最终结果
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Hachi_Lin/article/details/88092964