[计算机图形学]光栅化算法:DDA和Bresenham算法
一、DDA
DDA算法是最简单的直线绘制算法。主要思想是利用直线的斜截式:\(y=kx+b\)
对于一条直线的绘制,往往会给定两个端点:\(P_A = (0,0)\)和\(P_B = (60,60)\)
然后调用函数:OLED_DrawLine(0, 0, 60, 60);
首先,我们来看一下绘制直线都可以用哪些方法。
确定了两个端点,那么两个端点之间的点如何确定?
第一个可以想到:知道了两个点,可以用两点式来确定直线方程,然后带入点不就行了!
calc line expression f(x) from two point
for x in range(x_start, x_end):
y = f(x)
draw(x, y)
这是一个方法,不过这个方法可能要反复进行复杂数学运算(毕竟不可能所有的直线都是:\(y=x、y=x+1、...\)这类吧,但凡带点乘除、浮点的,统统都是复杂运算)
这个方法貌似不可行,不仅要确定直线的表达式,还要进行多次复杂的浮点数乘法运算。还记得在之前的文章(将三角形重心坐标时)简单介绍过线性组合...
巧了,这里正好有两个点,妥妥的线性组合哇。
线性组合公式:\(P' = tP_A + (1-t)P_B, 其中 t \in [0,1]\)
for t in range(0, accu, x num):
(x, y) = tPa + (1-t)Pb
draw(x, y)
虽然不用先计算直线的表达式,但是需要2N次的浮点数乘法。有点得不偿失。
这个方法也不得行。
那么可不可以每次让\(P_A\)加上一个值,\(P_B\)减去一个值,然后利用迭代的思想,逐个求出每一个点呢?
答案是可以的。
不妨用下面的形式进行稍微改进:
知道两个点,那么必然会知道某一个方向的步进次数(如果你乐意,甚至可以随便选取一个方向,我们这里选\(x\)方向),那么另一个方向(\(y\)方向)的步长\(\delta\)就知道了,虽然说确定步长会涉及到浮点除法运算,但是毕竟只用计算一次,还是可以接受的。然后根据步进次数,\(y=y'+\delta\),\(y'\)是上一个点的值(注意,虽然绘制到屏幕前要取整,但这里保存上一次值时保留取整前的。
calc y step value dy
y = y start
loop x num:
y += dy
draw(x, y)
可以很明显的察觉这个方法相比之前的暴力计算,计算量少了许多:整体来看,此方法用到一次浮点数除法,N次浮点数加法,相比N次浮点数乘法(暂且认为乘除是一样的,实际这种认为有失偏颇,毕竟大多时候能不用除法就不用)运算量降低了许多。
第三种方法便是DDA算法的前身。
但是DDA算法给出了更为明确的流程。
设当前点:\((x_i, y_i)\)
根据两个端点便可计算出\(\delta x\)和\(\delta y\)
则下一点:
\(x_{i+1} = x_i + \delta x\)
\(y_{i+1} = y_i + \delta y\)
根据前面提到的,需要一个方向的\(\Delta\)值为1,即每次步进一个像素
那么如何确定这个步进方向?
DDA算法给出明确的方法。
分别计算\(x\)和\(y\)方向上的差值\(\Delta x\)和\(\Delta y\)
- 如果\(\Delta x > \Delta y\),说明\(x\)轴变化的大,所以把\(x\)方向作为主步进方向,即:\(\delta x = 1, \delta y = \frac{\Delta y}{\Delta x} = k\)
- 如果\(\Delta y > \Delta x\),说明\(y\)轴变化的大,所以把\(y\)方向作为主步进方向,即:\(\delta y = 1, \delta x = \frac{\Delta x}{\Delta y} = \frac{1}{k}\)
仍然通过迭代的方式,即可求出每一个点。
可以看到,DDA算法去掉了浮点数乘法运算,仍需要多次浮点数加法运算和浮点数取整。因此还是有优化空间的。
二、Bresenham
Bresenham算法是一种基于误差判别式来生成直线的方法。
同样采用步进的思想,令每次最大变化方向的坐标步进一个像素单位(与DDA算法相同),另一个方向根据误差判别式的符号决定是否也要步进一个像素单位(与DDA算法不同)。
从Bresenham算法的思想描述中可以看出,本质上和DDA没有太大区别,只不过是另一个方向的步进值的确定产生了变化。
为什么在另一个方向上每次最大只步进一个像素?
这一点很好解释:
因为DDA算法和Bresenham算法都选取最大变化方向为主步进方向,这也就意味着,另一个方向的步进值无论是\(\delta y = \frac{\Delta y}{\Delta x}\)还是\(\delta x = \frac{\Delta x}{\Delta y}\)都必然小于等于1 。
另外,Bresenhan算法误差判别过程如下图所示。
那么,Bresenham算法和DDA算法区别在哪?就一个步进值么?
不是的,Bresenham算法和DDA算法的区别在于最后的光栅化过程(就是望屏幕上绘制的时候)。至于这个步进值的差异,不是很关键。
- DDA算法光栅化时,使用了一个浮点数转化宏:
#define FloatToInteger(fn) ((fn>0)?(fn+0.5):(fn-0.5))
- 而Bresenham算法光栅化时,使用的误差判别式
可以看到DDA始终于偏向选择无脑“步进”
如果Bresenham算法就到这,那也太low了:不仅没有去掉DDA算法中的浮点数加法运算,仅仅是为了让步进更丝滑而引入误差判别式,结果又引入了新的浮点数运算,图啥?
对此,改进版的Brehensam算法应运而生。
实际上,我没有去考证Bresenham在1965年发表Brehensam算法的论文,所以也不清楚那篇论文中就是改进后的。完了,不严谨了。
图片来自https://www.cnblogs.com/LiveForGame/p/11706904.html
\(d_1 = y - y_i = k(x_i + 1) + b - y_i\)
\(d_2 = y_{i+1} - y = (y_i + 1) - [k(x_i + 1) + b]\)
两式相减,得:\(d_1 - d_2 = 2k(x_i + 1) - 2y_i + 2b - 1\)
因为:\(k = \frac{\Delta y}{\Delta x}\)
所以:\(\Delta x (d_1 - d_2) = 2 \Delta y x_i + 2 \Delta y - 2y_i \Delta x + 2b \Delta x - \Delta x\)
又因为:\(\Delta y、\Delta x、b\)对于一条指向来说,是常量
所以:\(\Delta x (d_1 - d_2) = 2 \Delta y x_i - 2 \Delta x y_i + c\)
令:\(\Delta x (d_1 - d_2) = e_i\),\(e_i\)称为误差测量参数
若\(e_i > 0\),即:\(d_1 - d_2 > 0\),则实际点更靠近右上方的点(应选用右上方的点)
若\(e_i < 0\),即:\(d_1 - d_2 < 0\),则实际点更靠近右侧的点(应选用右侧的点)
若\(e_i = 0\),即:\(d_1 - d_2 = 0\),则随缘。实际不容易遇到,毕竟\(d_1、d_2\)都是浮点数,相等太难了(这一点参考浮点数的编码方式就知道了)
现在通过判断\(e_i\)的符号就可以判断下一个点是否需要步进了。
那么,如何去掉判别时的浮点运算呢?即如何确定\(d_1\)和\(d_2\)的值?
不忙,继续推导。
当前点的误差测量参数:\(e_i = 2 \Delta y x_i - 2 \Delta x y_i + c\)
下一点的误差测量参数:\(e_{i+1} = 2 \Delta y x_{i+1} - 2 \Delta x y_{i+1} + c\)
两式相减,得:\(e_{i+1} - e_i = 2 \Delta y x_{i+1} - 2 \Delta x y_{i+1} - [2 \Delta y x_i - 2 \Delta x y_i]\)
整理,得:\(e_{i+1} - e_i = 2 \Delta y (x_{i+1} - x_i) - 2 \Delta x (y_{i+1} - y_i)\)
又因为:\(x_{i+1} - x_i = 1\)
所以:\(e_{i+1} - e_i = 2 \Delta y - 2 \Delta x (y_{i+1} - y_i)\)
所以:
当选择右侧的点时:\(e_{i+1} = e_i + 2 \Delta y\)
选择右上角的点时:\(e_{i+1} = e_i + 2 \Delta y - 2 \Delta x\)
可以发现,并不需要确定\(d_1\)和\(d_2\)的值。
根据\(e_i\)的符号可以递推出下一点的误差判别参数\(e_{i+1}\),反过来根据这个新得到的误差判别参数,可以继续确定下下一点的误差判别参数...
递归,完美。
但是,初始的\(e_0\)怎么确定?
对于初始点:
因为:\(\Delta x (d_1 - d_2) = e_i\),所以\(\frac{e_0}{\Delta x} = d_1 - d_2\)
又因为:\(d_1 - d_2 = 2k(x_0 + 1) - 2y_0 + 2b - 1 = 2kx_0 + 2k - 2y_0 + 2b - 1\)
又因为:\(y_0 = kx_0 + b\)
所以:\(d_1 - d_2 = 2(kx_0 + b) + 2k - 2y_0 - 1 = 2\frac{\Delta y}{\Delta x} - 1 = \frac{e_0}{\Delta x}\)
所以:\(e_0 = 2 \Delta y - \Delta x\)
好了,初始点有了,递推公式也有了,剩下的就是写程序了。
至此,改进版的Brehenham算法全部推导完成。
后面会附上Brehensam算法绘制直线的C语言程序,可能和这里的推导过程由出入,但算法的核心是一样的。
三、绘制图形
1. 绘制直线
对于水平直线和垂直直线,大可不必通过算法去求解,毕竟这两类直线只在一个方向有步进,而另一个方向步进值始终为0。因此,对于这两种情况,可以单独讨论。
/**
* @brief :画线(像素坐标,左上为基点,右下增)
* @note :--
* @param :xStart, 行起始坐标(0~127)
yStart, 列起始坐标(0~63)
xEnd , 行终止坐标(0~127)
yEnd , 列终止坐标(0~63)
* @return :void
*
* @date :2016/09/09
* @design :
**/
void OLED_DrawLine(uint32_t xStart, uint32_t yStart, uint32_t xEnd, uint32_t yEnd)
{
int8_t x_width; //x轴宽度
int8_t y_height;//y轴高度
int8_t x_inc; //x方向自增标记
int8_t y_inc; //y方向自增标记
int8_t rem; //current remainder
uint8_t start, end;
uint8_t i;
if(yStart == yEnd)//绘制水平线,horizon line
{
if(xStart > xEnd)
{
start = xEnd;
end = xStart;
}else{
start = xStart;
end = xEnd;
}
for(i=start; i<=end; i++){
OLED_DrawPixelPoint(i, yStart, 1);
}
}else if(xStart == xEnd){//绘制垂直线,vertical line
if(yStart > yEnd)
{
start = yEnd;
end = yStart;
}else{
start = yStart;
end = yEnd;
}
for(i=start; i<=end; i++){
OLED_DrawPixelPoint(xStart, i, 1);
}
}else{//绘制任意直线
x_width = xEnd - xStart;
y_height = yEnd - yStart;
if(x_width < 0) x_width = 0 - x_width;
if(y_height < 0) y_height = 0 - y_height;
x_inc = (xEnd > xStart) ? 1 : -1;
y_inc = (yEnd > yStart) ? 1 : -1;
if(x_width >= y_height)
{
rem = x_width/2;
for(; xStart!=xEnd; xStart+=x_inc)
{
OLED_DrawPixelPoint(xStart, yStart, 1);
rem += y_height;
if(rem >= x_width)
{
rem -= x_width;
yStart += y_inc;
}
}
}else{
rem = y_height/2;
for(; yStart!=yEnd; yStart+=y_inc)
{
OLED_DrawPixelPoint(xStart, yStart, 1);
rem += x_width;
if(rem >= y_height)
{
rem -= y_height;
xStart += x_inc;
}
}
}
}
}
2. 绘制圆
没有什么特别的,主要注意利用圆的八分对称性,可以减少数学运算的次数。
同时使用改进版本,避免了浮点运算。
/**
* @brief :八分对称法(像素坐标)
* @note :--画出给定点的八分对称点(画圆基础算法)
* @param :xc, 圆心行坐标
yc, 圆心列坐标
x , 给定点
y , 给定点
* @return :void
*
* @date :2017/01/02
* @design :
**/
static void Circle8Point(uint32_t xc, uint32_t yc, uint32_t x, uint32_t y)
{
//直角坐标系第一象限x轴开始,逆时针旋转!
OLED_DrawPixelPoint((xc+x), (yc+y), 1);//1
OLED_DrawPixelPoint((xc+y), (yc+x), 1);//2
OLED_DrawPixelPoint((xc-y), (yc+x), 1);//3
OLED_DrawPixelPoint((xc-x), (yc+y), 1);//4
OLED_DrawPixelPoint((xc-x), (yc-y), 1);//5
OLED_DrawPixelPoint((xc-y), (yc-x), 1);//6
OLED_DrawPixelPoint((xc+y), (yc-x), 1);//7
OLED_DrawPixelPoint((xc+x), (yc-y), 1);//8
}
/**
* @brief :改进画圆(像素坐标)
* @note :--避免浮点运算(轴上点不突进!)!
* @param :xc, 圆心行坐标
yc, 圆心列坐标
r , 半径
* @return :void
*
* @date :2017/01/02
* @design :
**/
void OLED_DrawCircle(uint32_t xc, uint32_t yc, uint32_t r)
{
uint32_t x, y;
int32_t d;//改进,避免浮点运算!
x = 0;
y = r;
d = 3-2*r;
Circle8Point(xc ,yc, x, y);
while(x < y)
{
if(d < 0)
{
d += 4*x+6;
}else{
d += 4*(x-y)+10;
--y;
}
++x;
Circle8Point(xc, yc, x, y);
}
}
3. 绘制椭圆
和圆绘制过程类似,同样利用了椭圆的对称性。
/**
* @brief :四分对称法(像素坐标)
* @note :--画出给定点的四分对称点(画椭圆基础算法)
* @param :xc, 椭圆中心行坐标
yc, 椭圆中心列坐标
x , 给定点
y , 给定点
* @return :void
*
* @date :2017/01/04
* @design :
**/
static void Ellipse4Point(uint32_t xc, uint32_t yc, uint32_t x, uint32_t y)
{
//直角坐标系第一象限开始,逆时针旋转!
OLED_DrawPixelPoint((xc+x), (yc+y), 1);//1
OLED_DrawPixelPoint((xc-x), (yc+y), 1);//2
OLED_DrawPixelPoint((xc-x), (yc-y), 1);//3
OLED_DrawPixelPoint((xc+x), (yc-y), 1);//4
}
/**
* @brief :画椭圆(像素坐标)
* @note :--
* @param :xc, 椭圆中心行坐标
yc, 椭圆中心列坐标
a , 半长轴长度
b , 半短轴长度
* @return :void
*
* @date :2017/01/04
* @design :
**/
void OLED_DrawEllipse(uint32_t xc, uint32_t yc, uint32_t a, uint32_t b)
{
int32_t x=0;
int32_t y=b;
int32_t b2=(int32_t)b;
float sqa=a*a;
float sqb=b*b;
float d=sqb+sqa*(-b2+0.25f);
Ellipse4Point(xc, yc, x, y);
while((sqb*(x+1)) < (sqa*(y-0.5f)))
{
if(d < 0)
{
d += sqb*(2*x+3);
}else{
d += sqb*(2*x+3)+sqa*(-2*y+2);
--y;
}
++x;
Ellipse4Point(xc, yc, x, y);
}
d = (b*(x+0.5))*2 + (a*(y-1))*2 - (a*b)*2;
while(y > 0)
{
if(d < 0)
{
d += sqb*(2*x+2)+sqa*(-2*y+3);
++x;
}else{
d += sqa*(-2*y+3);
}
--y;
Ellipse4Point(xc, yc, x, y);
}
}
[计算机图形学]光栅化算法:DDA和Bresenham算法的更多相关文章
- Python使用DDA算法和中点Bresenham算法画直线
title: "Python使用DDA算法和中点Bresenham算法画直线" date: 2018-06-11T19:28:02+08:00 tags: ["图形学&q ...
- 计算机图形学之扫描转换直线-DDA,Bresenham,中点画线算法
1.DDA算法 DDA(Digital Differential Analyer):数字微分法 DDA算法思想:增量思想 公式推导: 效率:采用了浮点加法和浮点显示是需要取整 代码: void lin ...
- 《图形学》实验七:中点Bresenham算法画椭圆
开发环境: VC++6.0,OpenGL 实验内容: 使用中点Bresenham算法画椭圆. 实验结果: 代码: #include <gl/glut.h> #define WIDTH 50 ...
- 《图形学》实验六:中点Bresenham算法画圆
开发环境: VC++6.0,OpenGL 实验内容: 使用中点Bresenham算法画圆. 实验结果: 代码: #include <gl/glut.h> #define WIDTH 500 ...
- 《图形学》实验四:中点Bresenham算法画直线
开发环境: VC++6.0,OpenGL 实验内容: 使用中点Bresenham算法画直线. 实验结果: 代码: //中点Bresenham算法生成直线 #include <gl/glut.h& ...
- 计算机图形学——梁友栋-Barsky算法
梁算法是计算机图形学上最经典的几个算法,也是目前唯一一个以中国人命名的出现在国内外计算机图形学课本的算法,我之前在介绍裁剪算法的时候介绍过这个算法 https://www.cnblogs.com/wk ...
- CGA填充算法之种子填充算法
CGA填充算法之种子填充算法 平面区域填充算法是计算机图形学领域的一个很重要的算法,区域填充即给出一个区域的边界 (也可以是没有边界,只是给出指定颜色),要求将边界范围内的所有象素单元都修改成指定的颜 ...
- 计算机图形学(第2版 于万波 于硕 编著)第45页的Bresenham算法有错误
计算机图形学(第2版 于万波 于硕 编著)第45页的Bresenham算法有错误: 书上本来要写的是以x为阶越步长的方法,但是他写的是用一部分y为阶越步长的方法(其实也写的不对),最后以x为阶越步长的 ...
- 图形学入门(1)——直线生成算法(DDA和Bresenham)
开一个新坑,记录从零开始学习图形学的过程,现在还是个正在学习的萌新,写的不好请见谅. 首先从最基础的直线生成算法开始,当我们要在屏幕上画一条直线时,由于屏幕由一个个像素组成,所以实际上计算机显示的直线 ...
随机推荐
- Cisco的互联网络操作系统IOS和安全设备管理器SDM__管理Cisco互联网络
1.如果不能远程登录到一台设备上,可能是由于远程设备上没有设置口令.也可能是由于访问控制列表过滤了远程登录会话. show users:检查都有哪些设备连接到了此路由器. clear line #:清 ...
- Codeforces Round #681 (Div. 2, based on VK Cup 2019-2020 - Final)【ABCDF】
比赛链接:https://codeforces.com/contest/1443 A. Kids Seating 题意 构造一个大小为 \(n\) 的数组使得任意两个数既不互质也不相互整除,要求所有数 ...
- AC自动机——看似KMP在跑,其实fail在跳
先存代码 AC自动机(简单版) #include<bits/stdc++.h> #define maxn 1000007 using namespace std; int n,ans; i ...
- Codeforces 1355 E. Restorer Distance(三分)
传送门:E - Restorer Distance 题意:给出四个数 N, A, R, M ,然后给出一个长度为N的序列.让一个数+1花费A,-1花费R,从一个大的数向一个小的数移动1花费M.问让所 ...
- Educational Codeforces Round 85 (Div. 2)
题目链接:https://codeforces.com/contest/1334 A. Level Statistics 题意 一个关卡有玩家的尝试次数和通关次数,按时间顺序给出一个玩家 $n$ 个时 ...
- python+fiddler下载vip视频 && ts视频可合并
如果你只想在线看视频可以去看这篇博客:python实现通过指定浏览器免费观看vip视频 先看一下我们程序运行的结果 我们要解析的接口就是(就是这个"接口+视频地址"可以解析出vi ...
- AtCoder Beginner Contest 183 E - Queen on Grid (DP)
题意:有一个\(n\)x\(m\)的棋盘,你需要从\((1,1)\)走到\((n,m)\),每次可以向右,右下,下走任意个单位,\(.\)表示可以走,#表示一堵墙,不能通过,问从\((1,1)\)走\ ...
- Educational Codeforces Round 91 (Rated for Div. 2) A. Three Indices (模拟)
题意:有一长度为\(n\)的序列,问是否能找到\(a_{i}<a_{j},a_{j}>a_{k},(i<j<k)\),如果满足,输出其位置. 题解:直接暴力两头找即可,最坏复杂 ...
- 添加特定软件证书到windows不信任列表
$target="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" $filePath=$PSScript ...
- 敏捷史话(六):也许这个人能拯救你的代码 —— Robert C. Martin
Robert C. Martin( 罗伯特·C·马丁),作为世界级软件开发大师.设计模式和敏捷开发先驱.C++ Report杂志前主编,也是敏捷联盟(Agile Alliance)的第一任主席,我们尊 ...