计算机图形学 | 图形思维的起点——朴素的软光栅
华中科技大学《计算机图形学》课程
MOOC地址:计算机图形学(HUST)
计算机图形学 | 图形思维的起点——朴素的软光栅
4.1 初次尝试——点和直线
扫描转换的概念
光栅扫描式图形显示器:有一个点阵单元发生器,通过控制每个点阵单元的亮度来显示一副完整的图形。
和CRT相似,液晶显示器、LED显示器也都有一个点阵单元发生器,这个点阵单元其实就是像素点阵。
图形的光栅化(图形的扫描转换)分成两步:
- 根据图形的定义在点阵单元上确定最佳逼近于图形的像素集,逼近的过程本质可以认为是连续量向离散量的转换;
- 给像素指定合适的颜色值。
这个过程叫做图形的光栅化(图形的扫描转换):
一般而言,光栅化阶段由GPU完成。
我们也可以由程序员完成从图元到像素点的转换,这种不借助硬件提供的API,直接由应用程序计算的过程,叫做软光栅。
约定:为像素点阵建立一个坐标系,先考虑二维图形的生成。
点
输入:点的坐标
输出:像素点的位置
直线
输入:直线两个端点的坐标P0(x0,y0)和P1(x1,y1)
输出:最佳逼近这条直线的像素点集
高质量直线的要求:
- 直线要直,不能有锯齿效果
- 直线的端点要准确,即无定向性和断裂情况
- 直线的亮度、色泽要均匀
- 画线的速度要快,还能处理不同线宽、颜色、线型
数值微分算法
数值微分法(Digital Differential Analyzer,简称DDA)。
一种直接从直线的微分方程生成直线的方法。
通过给定直线的两端点坐标P0(x0,y0)和P1(x1,y1),我们可以得到直线的微分方程:
Ɛ足够小,计算精度可以无限高。
但设备的精度是有限的,令ε=1/max(|△x|,|△y|),使得ε△x戒ε△y中会有一个变成单位步长、
这就使得,算法在最大位移方向上,每次总是走一步。
情况一:
斜率绝对值小于1:ε=1/|△x|
情况二:
斜率绝对值大于1:ε=1/|△y|
后者(也就是情况二)的效率更高。
DDA直线生成算法:
void DDAline(int x0, int y0, int x1, int y1)
{
int dx, dy, eps1, k;
float x, y, xIncre, yIncre;
dx = x1 - x0;
dy = y1 - y0;
x = x0;
y = y0;
If(abs(dx) > abs(dy)) eps1 = abs(dx);
else eps1 = abs(dy);
xIncre = (float)dx / (float)eps1;
yIncre = (float)dy / (float)eps1;
for (k = 0; k <= eps1; k++)
{
putpixel((int)(x + 0.5), (int)(y + 0.5)); // 在对应坐标处输出像素点
x += xIncre;
y += yIncre;
}
}
DDA直线生成算法特点:
在一个迭代算法中,如果每一步的x、y值是用前一步的值加上一个增量来获得的,那么,这种算法就称为增量算法。因此,DDA算法是一个增量算法。
优点:DDA算法直观、易实现
缺点:有浮点数和浮点运算,效率不高
中点的Bresenham算法
输入:直线两个端点的坐标P0(x0,y0)和P1(x1,y1)
输出:最佳逼近这条直线的像素点集
通过给定直线的两端点坐标P0(x0,y0)和P1(x1,y1),可得到直线斜截式表示:y=kx+b。
我们还可以写出它的隐式方程:F(x,y)=y-kx-b=0,其中k=Δy/Δx=y1-y0/x1-x0。
基本原理:
假定0≤k≤1,x是最大位移方向
通过中点不直线的位置关系引入误差项d,判断误差项的符号来选择候选点。
M在Q的下方,取Pu;M在Q的上方,取Pd。
构造判别式d=F(xM,yM)=F(xi+1,yi+0.5)=yi+0.5-k(xi+1)-b。
- 当d<0时,取右上方Pu。
- 当d=0时,任取一个,约定取Pd
- 当d>0,取正右方的Pd
误差项递推:d﹤0
误差项递推:d≥0
d的初值
d的计算变成了增量计算,但计算过程仍然存在浮点数。
解决办法:将d放大2△x倍,消除浮点数。
在0≤k≤1情况下的整数的中点Bresenham算法:
- 输入直线的两端点P0(x0,y0)和P1(x1,y1)。
- 计算初始值△x、△y、d=△x-2△y、x=x0、y=y0。
- 绘制点(x,y)。
判断d的符号
若d<0,则(x,y)更新为(x+1,y+1),d更新为d+2△x-2△y;
否则(x,y)更新为(x+1,y), d更新为d-2△y。 - 当直线没有画完时,重复步骤3,否则结束。
实例:
输入:直线两个端点的坐标P0(0,0)和P1(8,5)
输出:最佳逼近这条直线的像素点集
改进的Bresenham算法
更直观的想法:
假定0≤k≤1,x是最大位移方向,每次xi+1=xi+1,
- d>0.5 yi+1=yi+1
- d=0.5 yi+1=yi
- d<0.5 yi+1=yi
d的变换规律:d的初值为0,d每次增加k,一旦向上走了一步则让d减去1。
改进一:
改进二:
在0≤k≤1情况下改进的Bresenham算法:
- 输入直线的两端点P0(x0,y0)和P1(x1,y1)。
- 计算初始值△x、△y、e=-△x、x=x0、y=y0。
- 绘制点(x,y)。
- e更新为e+2△y
判断e的符号
若e>0,则(x,y)更新为(x+1,y+1),同时将e更新为e-2△x;
否则(x,y)更新为(x+1,y)。 - 当直线没有画完时,重复步骤3和4。否则结束。
实例:
输入:直线两个端点的坐标P0(0,0)和P1(8,5)
输出:最佳逼近这条直线的像素点集
4.2 如果是圆?
问题描述:圆的扫描转换
简化问题:只考虑圆心在原点的圆
那么对于任意圆呢?
答:先将圆的中心扫描转换到原点,再扫描转换回原本位置。
八分法画圆
圆的对称性分析:位于原点的圆,可以被4条直线得到8个区域。这就是八分法画圆的由来。
简化问题:只需要画出一个八分之一段圆弧即可。
八分法画圆的问题描述:绘出圆心在原点,半径为整数R的圆x2+y2=R2。
基本思想:绘制出下图中八分之一段圆弧,利用对称的方法绘制出另外七段。
方法一:利用简单方程
基本思想:利用函数方程,直接离散计算。
x为最大位移方向,x每次增加1,每次计算y后四舍五入。
方法二:利用极坐标方程
基本思想:利用极坐标方程,直接离散计算。
步长Δθ选择对圆弧效果的影响:步长越小,圆越圆滑。
中点画圆法
假定0≤k≤1,x是最大位移方向判别式d=F(x,y)=x2+y2-R2。
问题的简化:只考虑1/8段圆弧。
与八分法画圆一样,最大位移是x方向,x每次增加1,y减少1或者不变。
构造判别式:d=F(M)=(xi+1)2+(yi-0.5)2-R2。
- 如果d>0,M在圆外,取Pd
- 如果d<0,M在圆内,取Pu
- 如果d=0,M在圆上,约定取Pu
第一种情况:d≤0,取Pu
第二种情况:d>0,取Pd
d的初值
d的整数化尝试
此时只需要判断d的符号。
中点Bresenham画圆法
- 输入圆的半径R
- 计算初始值
d=1.25-R
x=0
y=R - 绘制点(x,y)及其在八分圆中的另外七个对称点
- 判断d的符号
若d≤0,先将d更新为d+2x+3,再将(x,y)更新为(x+1,y);
否则,先将d更新为d+2(x-y)+5,再将(x,y)更新为(x+1,y-1) - 当x<y时,重复步骤3和4,否则结束
4.3 如果是圆?
椭圆的隐式方程:F(x,y)=b2x2+a2y2-a2b2。
椭圆的对称性:关于x轴对称,关于y轴对称。
所以只需要画出第一象限的椭圆弧,其他按对称给出。
第一象限椭圆弧分析:
实际算法:用中点近似判断
于是,
上一个中点2a2(yi-0.5)>2b2(xi+1),下一个中点2a2(yi-0.5)<2b2(xi+1),根据中点从上半部分转入下半部分。
上半部分:
第一种情况:d1≤0,取Pu
第二种情况:d1>0,取Pd
误差项初值
下半部分:
第一种情况:d2>0,取Pl
第二种情况:d2≤0,取Pr
判断上半部分是否转如到下半部分?
计算下半部分中点的初值
椭圆中点Bresenham算法:
- 输入椭圆的长半轴a和短半轴b
- 计算初始值
d=b2+a2(-b+0.25),x=0,y=b - 绘制点(x,y)及其另外三个对称点
- 判断d的符号
若d≤0,先将d更新为d+b2(2x+3),再将(x,y)更新为(x+1,y)
否则,先将d更新为d+b2(2x+3)+a2(-2y+2),再将(x,y)更新为(x+1,y-1) - 当2a2(y-0.5)>2b2(x+1)时,重复步骤3和4,否则转到步骤6
- 用上半部分计算的最后点(x,y)来计算下半部分中d的初值
d20=F(M)=b2(x+0.5)2+a2(y-1)2-a2b2 - 绘制点(x,y)及其在四分象限上的另外三个对称点。
- 判断d的符号
若d≤0,先将d更新为b2(2xi+2)+a2(-2yi+3),再将(x,y)更新为(x+1,y-1)
否则,先将d更新为d+a2(-2yi+3),再将(x,y)更新为(x,y-1) - 当y>0时,重复步骤7和8,否则结束。
4.4 遇见多边形
实例:
输入:多边形顶点序列P1(x1,y1)到P7(x7,y7)
输出:最佳逼近这个多边形的像素点集
X扫描线算法
算法步骤:
-
确定多边形所占有的最大扫描线数,得到多边形顶点的最小和最大y值(ymin和ymax)
-
从y=ymin到y=ymax,每次用一条扫描线进行填充
-
对一条扫描线填充的过程可分为四个步骤:求交、排序、交点配对、区间填色
当扫描线与多边形顶点相交时,交点的取舍问题
策略:
交点个数=构成这个顶点的两条边位于扫描线上方的条数
交点个数为0的点在扫描转换时被舍弃。
视觉膨胀
策略:左闭右开,下闭上开
缺点:形状保持中心偏移半个像素
算法的效率问题
对一条扫描线填充的过程可分为四个步骤:求交、排序、交点配对、区间填色。
求交、排序的开销较大。
改进的出发点1:对于某一条扫描线,需要与所有的边求交吗?
改进的出发点2:扫描线和直线在Y方向上都有连贯性,那么交点可以怎么求?
改进的出发点3:每次都需要排序吗?
改进措施:
- 只与有效边求交
- 计算交点时采用增量计算
- 有的边变成无效边时,需要删除;新边加入时,才重新排序。
有序边表算法
数据结构
边表(EdgeTable):
新边(NewEdge):
新边结点排序原则:
- 先按交点的x坐标递增
- 若交点的x坐标相同,按增量递增
实例:
策略:交点个数=构成这个顶点的两条边位于扫描线上方的条数
将涉及到的几条边的ymax减去1,这样,对应的交点就不会再被计算。
有效边(ActiveEdge):指与当前扫描线相交的多边形的边,也称为活性边。
有效边表(ActiveEdgeTable,AET):把有效边按与扫描线交点x坐标递增的顺序存放在一个链表中,此链表称为有效边表。有效边表初始化为空。
实例:
算法分析
优点:
- 采用增量计算的方法进行交点计算
- 仅仅在新边加入时排序(边数<<扫描线数)
缺点:桶表、链表的维护开销
边标志算法
输入:多边形顶点序列P1(x1,y1)到P7(x7,y7)
输出:最佳逼近这个多边形的像素点集
边缘填充算法:逐边向右取反
缺点:有重复访问的像素点
改进:栅栏填充算法
栅栏填充算法分为两个步骤:
- 打标记
- 填充
具体规则:每个顶点设置一个标志位,初值为false。遇到标记点取反。
若标志位为true,则填充;否则,不填充。
实例:
改进:先画边界后填色
边标志算法的基本思想:
- 用一种特殊的颜色在帧缓存器中将多边形的边界勾画出来;
- 将着色的像素点依x坐标递增的顺序两两配对;
- 将每一对像素所构成的扫描线区间内的所有像素置为填充色。
边标志算法分为两个步骤:
- 对多边形的每条边进行直线扫描转换(以多边形边界所经过的像素打上边标志)
- 填充
具体规则:每个顶点设置一个标志位,初值为false。遇到标记点取反。
若标志位为true,则填充;否则,不填充。
实例:
算法分析
- 边标志算法对每个象素仅访问一次。与边缘填充算法和栅栏填充算法相比,避免了对帧缓存中大量元素的多次赋值,但仍然需逐条扫描线地对帧缓存中的元素进行搜索和比较。
- 当用软件实现本算法时,速度与Y向连贯性算法相当,但本算法用硬件实现后速度会有很大提高。
4.5 巧妙的区域填充
区域的定义
区域的定义:指已经表示成点阵形式的填充图形,它是像素集合。
两种表示形式:
- 边界表示法:把位于给定区域的边界上的象素一一列举出来的方法称为边界表示法。
- 内点表示法:枚举出给定区域内所有象素的表示方法称为内点表示。
区域的分类:4连通和8连通。
定义:4-邻接点8-邻接点
定义:4连通边界表示8连通边界表示
定义:4连通内点表示8连通内点表示
区域填充算法的分类:
由4连通和8连通,有:4连通算法和8连通算法。
针对内点表示还是边界表示,有:边界填充(边界表示)和泛填充(内点表示)。
种子填充思想
种子的定义:边界表示区域内的任意一点或者内点表示区域的任意一点。
边界填充算法
算法输入:种子点坐标(x,y),填充色和边界颜色。
数据结构:栈结构
算法输出:最佳逼近的像素点集
4-连通边界填充算法步骤:
种子象素入栈,栈非空时重复执行三步操作:
- 栈顶象素出栈;
- 将出栈象素置成填充色;
- 检查出栈象素的4-邻接点,若其中某个象素点不是边界色且未置成多边形色,则把该象素入栈。
实例:
以此类推,后略。
4-连通泛填充算法步骤:
种子象素入栈;当栈非空时重复执行如下三步操作:
- 栈顶象素出栈;
- 将出栈象素置成填充色;
- 检查出栈象素的4-邻接点,若其中某个象素点是给定内部点的颜色且未置成新的填充色,则把该象素入栈。
分析与改进
问题1:8连通边界算法可以填充4连通的边界表示区域吗?
答:不能。
会导致填充错误。
问题2:8连通泛填充算法可以填充4连通的内点表示区域吗?
答:可以。
问题3:有重复入栈的现象,如何提高效率?
答:种子出栈时填充水平像素段。
4.6 属性:改变图元的模样
属性的定义
图元的外观由其属性来控制。
例如,线段可以是红色或蓝色,可以是实线或虚线,可以是粗线或细线等。区域的轮廓线作为线段可以有不同属性。区域的内部还有不同的属性。
如何实现属性
颜色
通过改变像素点的填充色来改变颜色。
线型
需要根据斜率的不同,调整像素模板。
线宽
线刷子的几个问题:
- 偶数个像素宽线宽会导致中心偏移半个像素
- 实际线宽比指定细,且对于不同的斜率的直线不同,这时需要根据斜率的不同,调整线刷子的方向
- 端点不自然,需要加额外的像素点
- 两条直线相交处也会有缺口,解决方法有:斜角连接、圆连接、斜切连接
另一种方法:方刷子
区域填充图案
图案对应像素模板:绘制像素点为1,其他为0。
指定填充纹理:
4.7 必不可少的反走样
走样的概念
走样现象:
- 阶梯状(锯齿感)
- 微小物体的忽略或时隐时现
走样的本质:用离散量表示连续量引起的失真。
反走样的方法
反走样的本质:用于减少或消除这种效果的技术。
最简单的方法:提高分辨率
受到这种方法的启发,我们可以先在高分辨率下取样计算,对几个象素属性进行平均,得到低分辨率下的像素属性。
于是我们得到下面2种方法:
过取样
过取样(supersampling),又叫后滤波。
简单过取样
降低锯齿感强的像素点的亮度,减弱整体锯齿感。
缺点:容易产生色泽不均匀的问题。
重叠过取样
基于加权模板的过取样
区域取样
区域取样(area sampling),又叫前滤波。
如何计算直线段不象素相交区域的面积?
可以利用一种求相交区域的近似面积的离散计算方法:
- 将屏幕象素分割成n个更小的子象素,
- 计算中心落在直线段内的子象素的个数m,
- m/n为线段不象素相交区域面积的近似值。
简单的区域取样特点:
- 直线段对一个象素亮度的贡献不两者重叠区域的面积成正比
- 相同面积的重叠区域对象素的贡献相同
加权区域取样原理:假想一个连续的加权曲面(或过滤函数)覆盖象素。当直线条经过该象素时,该象素的灰度值是在二者重叠区域上对滤波器(过滤函数)进行积分的积分值。
加权区域取样特点:
- 接近理想直线的象素将被分配更多的灰度值;
- 相邻两个象素的滤波器相交,有利于缩小直线条上相邻象素的灰度差。