活性边表算法c语言,图形学复习4——光栅化(画线画圆扫描线反走样算法)

图形学复习

CH7 光栅化

前几章介绍了几何处理和裁剪变换,接下来的步骤就是光栅化

光栅化是将形式表示的几何图元转换为阵列表示的数据片元的过程,片元中每一个像素对应帧缓冲区中的每一个像素

7.1 线段生成算法

(1)DDA画线算法

设直线表达式为y=mx+b,输入直线两端点坐标(x0,y0)和(xend,yend),可以计算出m=yend?y0xend?x0和b=y0?m?x0

DAA是基于微分运算的线段生成算法,其主要计算式便是δy=mδx:

若|m|≤1则x方向的变化大于y方向的变化,以x方向为主方向,取δx=1根据m计算δy=mδx

若|m|>1则y方向的变化大于x方向的变化,以y方向为主方向,取δy=1根据m计算δx=1mδy

为了有效的避免了斜率为正无穷时xend?x0=0的除零计算,我们将不直接计算m而是直接比较Δy=|yend?y0|和Δx=|xend?x0|的大小确定步长,计算出步长后每一步从的(x,y)更新到(x+xstep,y+ystep)并计算取整即可(注意,像素永远是整数点)

下面是DDA算法C语言版本代码:

void lineDDA(int x0, int y0, int xend, int yend){

int steps, k;

float xstep, ystep;

float x = x0, y = y0;

int dx = xend - x0;

int dy = yend - y0;

if (fabs(dx) >= fabs(dy))

steps = dx;

else

steps = dy;

xstep = (float)dx / (float)steps;

ystep = (float)dy / (float)steps;

setPixel(round(x), round(y));

for (k = 0; k < steps; k++) {

x += xstep;

y += ystep;

setPixel(round(x), round(y));

}

}

DDA算法避免的迭代时的乘积运算因此比直接用直线表达式求点坐标效率更高,但是,每一步骤中的浮点操作和取整运算开销仍然较大(体系结构告诉我们整数运算和浮点运算效率可以相差几十倍)

(2)Bresenham画线算法

Bresenham画线算法是一种精确而有效的线段生成算法,它运用DDA的思想并通过邻近点的比较避免了浮点操作和取整运算,下面讨论斜率小于1的线段的Bresenham画线算法(斜率大于1只需要类似DDA中的比较、交换即可)

再进一步方便讨论,限定0≤m≤1,我们需要确定的是当前画了点(xk,yk)之后下一步要画的点(xk+1,yk+1)的位置,由于x是主方向所以取xk+1=xk+1毋庸置疑,而x是主方向说明了ystep≤xstep=1,则yk≤yk+1≤yk+1,由于只能去整数点那么yk+1就只能是yk、yk+1二者其中之一,很显然我们只用选一个离yk+1近的就可以了

令dlower=yk+1?yk=f(xk+1)?yk=m(xk+1)+b?yk>0

令dupper=(yk+1)?yk+1=(yk+1)?f(xk+1)=(yk+1)?[m(xk+1)+b]>0

则要比较的就是dlower和dupper,选更小的那个即可,做差有:

dlower?dupper=2[m(xk+1)+b]?2yk?1

那么每次我们可以带入上式计算大于0(dlower更大更靠近上者)yk+1选yk+1,否则选yk,但是上式计算仍然有乘法计算,我们想避免乘法计算获取更高的效率,那么需要引入决策参数Δp(Bresenham算法核心),令:

Δp=Δx(dlower?dupper)=Δx[2m(xk+1)+2b?2yk?1]=Δx[2m(xk+1)+2b?2yk?1]=Δx[2ΔyΔx(xk+1)+2b?2yk?1]=2Δy?xk?2Δx?yk+c其中c=2Δy+2Δx?(2b?1)是常数

这样仍然存在乘法计算,考虑pk的迭代计算有:

pk+1=pk+2Δy?2Δx(yk+1?yk)

p0=2Δy?Δx,pk决定yk+1?yk取0还是1,因此只用一个if语句即可避免每次迭代中的乘法运算(把2Δy、2Δx存在临时变量中)

上述即是Bresenham画线算法的思想,下面给出二维全空间的**Bresenham画线算法**C语言源代码:

void line_Bresenham(int x1, int y1, int x2, int y2){

int dx = abs(x2 - x1);

int dy = abs(y2 - y1);

bool morethan_45 = dy > dx;

if (morethan_45) {

swap(x1, y1);

swap(x2, y2);

dx = abs(x2 - x1);

dy = abs(y2 - y1);

}

if (x1 > x2) {

swap(x1, x2);

swap(y1, y2);

}

int ystep = y2 > y1 ? 1 : -1;

int y = y1;

int x = x1;

int twody = 2 * dy;

int twody_minus_twodx = 2 * dy - 2 * dx;

int p = twody - dx;

while (x <= x2) {

if (morethan_45) {

plot_point(y + size, x);

} else {

plot_point(x, y + size);

}

if (p < 0) {

p += twody;

} else {

p += twody_minus_twodx;

y += ystep;

}

x++;

}

}

对称说明:

我们在1.Bresenham画线算法中给出的算法是在0 <= m <= 1的情况下的,所以对于全二维空间的直线我们需要做两次对称。

(1)x轴对称

做x轴对称将m取值范围扩展到|m| <= 1:

其他不变,引入变量ystep表示y的运动方向和步长。

0 <= m <= 1时,ystep = 1;-1 <= m < 0时,ystep = -1。

那么算法的第四步迭代y坐标时将1换成ystep即可。

(2)y=x对称

处理完|m| <= 1情况后,再处理|m| > 1的情况:

任意一条|m| > 1的直线都和一条|m| <= 1的直线关于y=x对称,那么则没必要写一次y方向为主方向的画线算法。直接在输入坐标后将端点坐标做一次y=x对称变换,当作一条|m| <= 1的直线来完成Bresenham画线算法的相应计算。最后在画点时再做一次y=x对称变换得到正确位置即可。

7.2 圆和椭圆生成算法

(1)中点画圆算法

中点画圆算法和Bresenham画线算法一样,通过引入决策参数来消除浮点和乘法运算

把圆划分成8个18圆(弧),可以通过做对称变换画出整个圆,下面以一象限内0

从x=0到x=y枚举点,在弧A上x的变化大于y的变换(x是主方向),那么画了(xk,yk)之后下一步要画的点(xk+1,yk+1)只能取(xk+1,yk)或(xk+1,yk?1)

中点画圆的思想就是:判断如果yk+1取yk和yk?1的中点yk?12,改点在圆外还是圆内

取决策参数pk=fcircle(xk+1,yk?12)=(xk+1)2+(yk?12)2?r2,则

pk<0,中点位于圆内,yk更靠近圆周边界

否则,yk?1更靠近圆周边界

同样有迭代思想求pk:

p0=54?r

若pk<0,则迭代yk+1=yk,2yk+1=2yk,pk+1=pk+2xk+1+1

否则,迭代yk+1=yk?1,2xk+1=2xk,2yk+1=2yk?2,pk+1=pk+2xk+1+1?2yk+1

两种情况均有xk+1=xk+1,2xk+1=2xk+2

注意上述描述的是以原点为中心点的画圆算法,若中心点需要在(xc,yc)的话,所有画的点均平移到(x+xc,y+yc)即可

(2)中点画椭圆算法

中点画椭圆算法和中点画圆算法基本一样,只是需要在一象限分两个区域1、2讨论,令fellipse=r2yx2+r2xy2?r2yr2x

区域1中:

p1k+1=?????p1k+2r2yxk+1+r2y,p1k<0p1k+2r2yxk+1+r2y?2r2xyk+1,p1k≥0

区域2中:

p2k+1=?????p2k?2r2xyk+1+r2x,p2k<0p2k?2r2xyk+1+r2y?2r2yxk+1,p2k≥0

注意区域2中小于0选(xk,yk?1),否则选(xk+1,yk?1)

7.3 通用扫描线填充算法

多边形扫描线填充的方法是沿多条平行于x轴的直线y=c逐像素扫描,若像素点落在多边形内部则填充为指定颜色

(1)扫描线算法

扫面线算法利用以下相邻像素连贯性,提高扫描效率:

边的连贯性:某条边与扫描线相交,它可能和下一条扫描线也相交

扫描线的连贯性:当前扫描线的交点顺序可能和下一条扫描线的交点顺序相同或类似

区间的连贯性:同一区间上像素取相同颜色填充

扫描线算法的一般步骤为:

求交点

交点排序

交点配对

(2)有序边表算法

有序边表算法就是一种经典的扫描线算法

定义活性边是与当前扫描线相交的边,边结构如下:

typedef struct {

int ymax; //边最大y值,即与相交的最大扫面线号

float x; //当前扫描线与边相交的x坐标

float dx; //边斜率的倒数

Edge *next; //指向下一条边的指针

} Edge;

那么活性边表AEL就是活性边按x递增的顺序构成的链表,和活动边表相关的是当前的扫描线,随着扫描线的移动按照如下的规则维护AEL:

如果一条边和下一条扫描线有交点,则根据该边的边斜率倒数dx来更新和下一条扫面线相交的x坐标点,即x=x+dx

如果一条边和下一条扫描线没有交点了,那么这条边不再是活动边,则删除AEL中的这条边

如果有其它新边和下一条扫描线相交,那么需要把新边加入AEL

上述新边的定义是:若某条(非水平)边的下端点是y,那么称之为扫面线y的新边;很容易发现要构造AEL就先要构造定义新边表NET,NEL无序排列因此构造NET枚举一遍边即可

下面给出有序边表算法的算法描述:

枚举所有边,构造NET

取y的初始值为NET中最小的非空元素,即最低扫描线的y坐标

置AEL为空

(循环开始)若NET中扫描边y的新边不为空则将所有边“取出”并插入AEL,AEL排序

AEL边中的x两两配对(x按取整规则取整),获取了有效区间后在第y行填充各区段

y=y+1

AEL中删除ymax=y的边

AEL中剩下的每条边做x更新,x=x+dx

(跳转)若AEL和NET有一个非空则转第4步循环

7.4 反走样

(1)走样与反走样策略

走样是指:用离散的像素来连续的图形时引起的失真,常见的走样有:

阶梯状边界(阶梯状斜线、阶梯状圆弧)

图形细节失真(图形比一个像素点小但被生成算法扩充成一个像素,细节展现削弱)

狭小图形直接遗失(图形比一个像素点小并且直接被生成算法“抛弃”,细节遗失)

反走样有一下几种策略:

提高分辨率

非加权区域采样

加权区域采样

提高分辨率可取但成本高,显示器首先要重新设计(提高1倍的分辨率需要4倍的像素点阵和帧缓存容量)其次图元生成、片元生成等算法开销均要增大

区域采样是把直线段看作具有一定宽度的狭长矩形,当直线段与某象素有交时,求出两者相交区域的面积,根据相交区域的面积,加权或不加权地确定该象素的亮度值,如下图展示:

活性边表算法c语言,图形学复习4——光栅化(画线画圆扫描线反走样算法)_第1张图片

非加权区域采样需要直接计算相交的三角形或梯形或其他多边形的区域的面积,运算量大(设计乘法运算),一种近似策略是分割像素点为更小的子像素,计算子像素落在直线内部的比例从而得到近似面积

非加权区域采样有一个问题就是像素中靠外面的子像素对近似面积的贡献和靠里面的子像素对近似面积的贡献相同,这样的得到的反走样图像不是很平顺,一般采用加权区域采样

我们可以仿照图像处理中滤波器的思想来设计反走样立方体滤波器以及圆锥体滤波器等等来确定权重

(2)直线反走样算法

直线反走样算法中应用最广泛的是Wu直线反走样算法,Wu反走样的算法和Bresenham画线算法思想很类似,同样是在比较dupper和dlower大小:

活性边表算法c语言,图形学复习4——光栅化(画线画圆扫描线反走样算法)_第2张图片

设背景色是C1,线条颜色是C2,那么如图的H点和L点的颜色分别是C1dupper+C2dlowerdupper+dlower、C1dlower+C2dupperdupper+dlower

对于黑线白背景我们用RGBA颜色的不透明度alpha表示灰度颜色,且栅格距离为1,则有H、L点不透明度分别为dlower、dupper

因此Wu反走样计算十分简单,直观上也能理解:线条离H点更近因此H点有更大的不透明度(dlower),L点反之;当线条过HL中点时H、L两个像素点的颜色应该具有同样的不透明度,符合常理

dlower、dupper的计算我们通过维护error——直线上坐标与画点坐标偏差来完成:

当0 < error < 0.5时,y(xk+1) > yk 且主点是(xk+1, yk)在下,dupper=1 - error,dlower=error,即主点不透明度1 - error,副点不透明度error。

当-0.5 <= error <= 0时,y(xk+1) < yk 且主点是(xk+1, yk)在上,dupper=1 + error,dlower=-error,即主点不透明度1 + error,副点不透明度-error。

Bresenham画线算法稍加修改即可得到Wu直线反走样算法,略

(3)多边形填充区域边界反走样算法

多边形填充区域边界反走样算法和直线反走样算法类似,只是它只考虑多边形外部边界的反走样,内部均填充为多边形指定颜色

或者直接按直线反走样算法,然后做扫描填充时不考虑边界像素点,只按最大亮度/不透明度填充内部像素点即可

你可能感兴趣的:(活性边表算法c语言)