[计算机图形学]光栅化算法: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)
开一个新坑,记录从零开始学习图形学的过程,现在还是个正在学习的萌新,写的不好请见谅. 首先从最基础的直线生成算法开始,当我们要在屏幕上画一条直线时,由于屏幕由一个个像素组成,所以实际上计算机显示的直线 ...
随机推荐
- 2021 年写 JavaScript 代码的 17 个优化技巧
我们经常会写一些 JavaScript 代码,但是如何写出干净又易维护的代码呢?本文将讲解 17 个 JavaScript 代码的技术帮助你提高编程水平,此外,本文可以帮助您为 2021 年的 Jav ...
- Effective Java读书笔记--对所有对象都通用的方法
1.覆盖equals请遵守通用规定.不需要覆写equals的场景:a.类的每个实例都是唯一的.b.类不需要提供"逻辑相等"的测试功能.c.超类已经覆盖了equals的方法.d.类是 ...
- 图片轮播展示效果-2D实现
图片的轮播展示效果如果使用2D实现,需要将3D中存在的近大远小效果使用图片的缩放呈现,因此需要存储和计算图片的位置同时还要计算存储图片的缩放信息.将所有图片的位置连线看作是一个椭圆,就可以根据图片的个 ...
- TypeScript中 typeof ArrayInstance[number] 剖析
假设这样一个场景,目前业务上仅对接了三方支付 'Alipay', 'Wxpay', 'PayPal', 实际业务 getPaymentMode 会根据不同支付方式进行不同的付款/结算流程. const ...
- 记一次由sql注入到拿下域控的渗透测试实战演练(下)
本次渗透使用工具: mimikatz,PsExec 本次渗透网络拓扑图: 开始渗透: 上一篇文章中我们讲到我们成功渗透了目标内网中web服务器并提取其管理员明文密码,通过明文密码又成功登陆了内网中的另 ...
- hdu4126Genghis Khan the Conqueror (最小生成树+树形dp)
Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 327680/327680 K (Java/Others) Total Submiss ...
- 【noi 2.5_1792】迷宫(bfs 或 dfs)
简单搜索,在n*n的矩阵中,问从起点是否可以到达终点,有些格子不可走,上下左右四个方向都可以走.(N<=100)1.bfs从起点开始走,直到走到终点或全部遍历过一次就结束.2.dfs要一走到终点 ...
- Codeforces Round #676 (Div. 2) XORwice、Putting Bricks in the Wall、Palindromifier
题目链接:XORwice 题意:给你两个数a.b.求一个数x,使得((a异或x)+(b异或x))这个值最小,输出最小那个x 题解: 输出(a|b)-(a&b)就行(猜了一手 代码: #incl ...
- GYM101810 ACM International Collegiate Programming Contest, Amman Collegiate Programming Contest (2018) M. Greedy Pirate (LCA)
题意:有\(n\)个点,\(n-1\)条边,每条边正向和反向有两个权值,且每条边最多只能走两次,有\(m\)次询问,问你从\(u\)走到\(v\)的最大权值是多少. 题解:可以先在纸上画一画,不难发现 ...
- Service Cloud 零基础(四)快速配置一个问卷调查(无开发)
本篇参考:https://trailhead.salesforce.com/content/learn/modules/survey-basics 我们在工作和生活中会经历过形形色色得调查问卷,有一些 ...