参考其他:http://liar.pangwa.com/2009/04/12/bresenham/
直线光栅化
直线光栅化是指用像素点来模拟直线 . 比如下图中用蓝色的像素点来模拟红色的直线 . 图中坐标系是显示器上的坐标系 : x 轴向右 , y 轴向下 .
设 deltaX = endX – startX, deltaY = endY – startY. 那么斜率为 k = deltaY / deltaX. 我们先考虑简单的情况 : 当 0 < k < 1 即直线更贴近 x 轴 . 在这种情况下 deltaY < deltaX, 所以在光栅化的过程中 , 在 y 轴上描的点比在 x 轴上描点少 . 那么就有一个很直观的光栅化算法 :
line_bresenham(startX, startY, endX, endY)
{
deltaX = endX - startX;
deltaY = endY - startY;
k = deltaY / deltaX;
for (x = startX, y = startY; x <= endX; ++x)
{
if ( 满足一定条件 )
{
++y;
}
drawPixel(x, y);
}
}
基于斜率 / 距离的两个简单直线光栅化算法
好了,貌似很简单 , 就剩一个问题 : “ 满足一定条件 ” 是什么 ? 可以用斜率判断 , 也可以用上图中直线与光栅线交点 ( 红点 ) 与光栅点 ( 蓝点 ) 的距离来判断 . 继续用伪代码说话 :
// 算法 1: 用斜率判断
void line_bresenham_k(startX, startY, endX, endY)
{
deltaX = endX - startX;
deltaY = endY - startY;
k = deltaY / deltaX;
for (x = startX, y = startY; x <= endX; ++x)
{
if (x - startX != 0)
{
// 计算当前斜率
currentK = (y - startY) / (x - startX);
// 如果当前斜率 < k, 则增加 y 坐标
if (currentK < k)
{
++y
}
}
drawPixel(x, y);
}
}
// 算法 2: 用距离判断 . 计算直线与光栅线交点 y 坐标我们需要用到
// 直线方程 y = k (x - startX) + startY
line_bresenham_dist(startX, startY, endX, endY)
{
deltaX = endX - startX;
deltaY = endY - startY;
k = deltaY / deltaX;
for (x = startX, y = startY; x <= endX; ++x)
{
// 计算直线与光栅线交点的 y 坐标 , 以及与光栅点的距离
ptY = k * (x - startX) + startY;
dist = ptY - y;
// 如果距离 > 0.5 或者 < -0.5, 说明我们需要增加 y 以
// 将距离的绝对值控制在 0.5 之类
if (dist > 0.5 || dist < -0.5)
{
++y;
}
drawPixel(x, y);
}
}
消灭浮点数 !
以上都是很直观的算法 , 下面不直观的来了 – 上面的算法都需要在循环体内执行乘法 , 准确的说 , 是进行浮点数的乘法 . 我们怎么能减少这些浮点数的乘法开销呢 ? 以基于距离的算法 2 为例 : 首先 , k 是一个浮点数 , 0.5 也是浮点数 . 我们可以通过将这些表达式都乘以 2 * deltaX ( 整数 ) 来解决浮点数的问题 . 伪代码 :
// 算法 3: 在算法 2 的基础上消灭浮点数 !
line_bresenham_dist(startX, startY, endX, endY)
{
deltaX = endX - startX;
deltaY = endY - startY;
for (x = startX, y = startY; x <= endX; ++x)
{
// 计算直线与光栅线交点的 y 坐标 , 以及与光栅点的距离
ptY1 = deltaY * (x - startX) + startY * deltaX;
dist1 = ptY1 - y * deltaX;
dist1 = dist1 << 1; // dist1 = dist1 * 2
// 如果距离 > 0.5 或者 < -0.5, 说明我们需要增加 y 以
// 将距离的绝对值控制在 0.5 之类
if (dist1 > deltaX || dist < -deltaX)
{
++y;
}
drawPixel(x, y);
}
}
解析如何消灭浮点数
设 ptY 为直线与 Y 轴交点
ptY = k * (x - startX) + startY; 等同于 ptY = dy/dx * (x – startX) + startY; 其中 dy/dx 是浮点数 , 两边同时乘以 dx 即可消灭浮点数。
ptY*dx = dy*(x – startX) + dx*startY;
即 dy*(x – startX) + dx*startY – y*dx 应 > 0.5 或 < -0.5 。而 0.5 也是浮点数,则两边同时乘以 2.
2*dy*(x – startX) + 2*dx*startY – 2*y*dx 应 > 1 或 < -1 。而目前考虑直线在第一象限,则只要考虑 2*dy*(x – startX) + 2*dx*startY – 2*y*dx > 1 。
算法为
line_bresenham_dist (int startX , int startY , int endX , int endY )
{
int dx = endX - startX ;
int dy = endY - startY ;
for (int x = startX , int y = startY ; x <= endX ; ++x )
{
if (2*dy *(x – startX ) + 2*dx *startY – 2*y *dx > 1)
{
y ++;
}
drawPixel (x , y );
}
}
消灭乘法 !
圆满解决浮点数运算问题 ! 不过 … 乘法运算还在 . 消灭乘法问题的办法比较不直观 , 让我们想一想 : 还有什么办法能简化运算 . 直线方程已经不能再简化 , 所以唯一的突破口就是能不能利用递推 / 用上一次循环的计算结果推导下一次循环的计算结果 .
首先我们来看看在算法 2 的基础上 ( 因为算法 2 计算红点蓝点之间的距离 , 比较直观 ), 怎么通过第 n – 1 次循环计算出的 dist 值 ( 设为 d1) 来推导出第 n 次循环的 dist 值 ( 设为 d2). 先回顾一下 : dist = 直线与光栅线交点的 y 坐标 – 相应光栅点的 y 坐标 . 我们从几何上直观地考虑 : 在第 n 次循环中 , 我们先根据上一次循环所计算出来的 d1, 暂时令 d2 = d1 + k, 因为我们要保证 -0.5 < d2 < 0.5, 而 d1 + k 满足 d1 + k > –0.5, 所以我们只需要考虑当 d1 + k > 0.5 时 , 我们需要将光栅点 y 坐标增加 1, 并且将 d2 减去 1. 显然 , 设 y1 是第 n – 1 次循环中光栅点的 y 坐标 , y2 是第 n 次循环中光栅点的 y 坐标 . 我们有
1) d2 = d1 + k – (y2 – y1)
2) 当 d1 + k > 0.5 时 y2 = y1 + 1, 否则 y2 = y1
我们已经能根据上面的两个关系式写出算法 , 不过为了消除乘法和浮点数 , 我们将这两个关系式两端同时乘以 2 * deltaX, 并且设 e = 2 * deltaX * d, 则我们有
3) e2 = e1 + 2 * deltaY – 2 * deltaX * (y2 – y1)
4) 当 e1 + 2 * deltaY > deltaX 时 y2 = y1 + 1, 否则 y2 = y1
终于 , 没有了乘法 (2 * deltaY 在循环体外计算且被简化为左移一位的运算 ), 没有了浮点数 , 根据关系式 3) 和 4), 写出算法 :
// 算法 4: 在算法 2, 3 的基础上利用递推消灭乘法和浮点数 !
line_bresenham(startX, startY, endX, endY)
{
deltaX = endX - startX;
deltaY = endY - startY;
e = 0;
deltaX2 = deltaX << 1;
deltaY2 = deltaY << 1;
drawPixel(startX, startY);
for (x = startX + 1, y = startY; x <= endX; ++x)
{
// 关系式 3) e2 = e1 + 2 * deltaY – 2 * deltaX * (y2 – y1)
// 关系式 4) 当 e2 + 2 * deltaY > deltaX 时 y2 = y1 + 1, 否则 y2 = y1
e += deltaY2;
if (e > deltaX)
{
e -= deltaY2;
++y;
}
drawPixel(x, y);
}
}
解析如何消灭乘法
设 e 为误差 , 开始为 0 。
line_bresenham_dist (int startX , int startY , int endX , int endY )
{
double dx = endX - startX ;
double dy = endY - startY ;
double k = dy /dx ;
double e = 0.0;
drawPixel (startX ,startY );
for (int x = startX +1, int y = startY ; x <= endX ; ++x )
{
e += k ;
if (e > 0.5)
{
e -= 1;
y ++;
}
drawPixel (x , y );
}
}
两边同时乘以 2*dx
line_bresenham_dist (int startX , int startY , int endX , int endY )
{
int dx = endX - startX ;
int dy = endY - startY ;
int dx2 = dx <<1;
int dy2 = dy <<1;
int e = 0*2*dx ;
drawPixel (startX ,startY );
for (int x = startX +1, int y = startY ; x <= endX ; ++x )
{
e += dy2 ;
if (e > dx )
{
e -= dx2 ;
y ++;
}
drawPixel (x , y );
}
}