[计算机图形学]光栅化算法: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)
开一个新坑,记录从零开始学习图形学的过程,现在还是个正在学习的萌新,写的不好请见谅. 首先从最基础的直线生成算法开始,当我们要在屏幕上画一条直线时,由于屏幕由一个个像素组成,所以实际上计算机显示的直线 ...
随机推荐
- YOLOv1论文解读
摘要 我们提出一种新的目标检测算法--YOLO.以前有关目标检测的研究将检测转化成分类器来执行.然而,我们将目标检测框架化为空间分隔的边界框及相关的类概率的回归问题.在一次评估中,单个神经网络直接从整 ...
- CentOS7.X安装英伟达显卡采坑之路
1.系统信息 操作系统版本:CentOS7.X 显卡版本:英伟达 Tesla P100 其他软件包安装信息: CUDA 9.0 CUDNN 7.4.2.24 lightgbm 2.2.X Boost ...
- 一次小模块的使用过程-LC12S无线模块介绍
前言: 最近帮人做了个小设备,使用了无线模块.触摸芯片,主要功能就是把触摸按键的信号无线传到控制继电器输出,MCU是STM8系列的芯片,其中使用过程中调试无线模块LC21S觉得挺好用的,就写了这篇文章 ...
- vs中python包安装教程
vs安装python很简单,只需要在vs安装包中选择python就可以了,这里使用的python3.7: 如果有了解,都知道安装python包的指令:"pip install xxx&quo ...
- 《C++ Primer》Chapter 7 [类]
前言 在C++中,我们使用类定义自己得数据类型/通过定义新的类型来反应待解决的题的各种概念,是我们更容易编写.调试和修改程序. 我们需要主要关注数据抽象的重要性.数据抽象能帮助我们将对象的具体实现与对 ...
- Codeforces Round #652 (Div. 2) E. DeadLee(贪心)
题目链接:https://codeforces.com/contest/1369/problem/E 题意 Lee 有 $n$ 种不同种类的食物和 $m$ 个朋友,每种食物有 $w_i$ 个,每个朋友 ...
- HDU-3499Flight (分层图dijkstra)
一开始想的并查集(我一定是脑子坏掉了),晚上听学姐讲题才知道就是dijkstra两层: 题意:有一次机会能使一条边的权值变为原来的一半,询问从s到e的最短路. 将dis数组开成二维,第一维表示从源点到 ...
- hdu3247Resource Archiver (AC自动机+最短路+状压dp)
Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 100000/100000 K (Java/Others) Total Submis ...
- 抓取QQ音乐歌单
抓取QQ音乐歌单1.通过分析歌曲下载路径来分析所需参数: 通过比较, 得出其中歌曲下载url与参数vkey是可变的,歌曲下载url中可变得值是请求歌单返回的歌曲数据的strMediaMid参数, 而v ...
- Windows Terminal 更换主题
1. 打开设置,是个json文件 2. 在此处获取主题配置:https://atomcorp.github.io/themes/ 3.将主题配置粘贴到schemes节点(可以增加N个) 4.配置每个命 ...