Bresenham 直线算法是计算机图形学中最经典的算法之一。它的核心魅力在于高效:它完全通过整数加减法和位移运算来决定像素点的路径,彻底告了诉昂贵的浮点运算和除法。
简单来说,给定两个点 (x0,y0)(x_0, y_0)(x0,y0) 和 (x1,y1)(x_1, y_1)(x1,y1),算法的任务是决定在显示器上哪些像素点最接近理想的直线。
1. 核心逻辑:二选一的决策
假设我们绘制斜率 0<m<10 < m < 10<m<1 的直线。当我们确定了当前点 (x,y)(x, y)(x,y) 后,下一个像素点只有两种选择:
- 水平向右 :(x+1,y)(x+1, y)(x+1,y)
- 右上对角 :(x+1,y+1)(x+1, y+1)(x+1,y+1)
算法通过维护一个误差变量(Decision Parameter) ddd 来判断:理想直线距离上面那个像素近,还是距离下面那个近?
2. 算法步骤(以斜率 0≤m≤10 \le m \le 10≤m≤1 为例)
- 计算增量:
- Δx=x1−x0\Delta x = x_1 - x_0Δx=x1−x0
- Δy=y1−y0\Delta y = y_1 - y_0Δy=y1−y0
- 初始化决策参数:
d0=2Δy−Δxd_0 = 2\Delta y - \Delta xd0=2Δy−Δx
要把 Bresenham 算法中这个神秘的初始决策参数 d0=2Δy−Δxd_0 = 2\Delta y - \Delta xd0=2Δy−Δx 讲清楚,我们需要回到它的几何本质:中点判别法(Midpoint Method)。
其核心思想是:观察理想直线与两个候选像素点之间的"中点"的关系。
1. 定义直线方程
假设我们要画一段从 (x0,y0)(x_0, y_0)(x0,y0) 到 (x1,y1)(x_1, y_1)(x1,y1) 的直线,斜率 0<k<10 < k < 10<k<1。
我们可以将直线方程表示为隐式方程 F(x,y)=0F(x, y) = 0F(x,y)=0:
y=ΔyΔxx+By = \frac{\Delta y}{\Delta x}x + By=ΔxΔyx+B
展开并整理得到:
F(x,y)=2Δy⋅x−2Δx⋅y+2Δx⋅B=0F(x, y) = 2\Delta y \cdot x - 2\Delta x \cdot y + 2\Delta x \cdot B = 0F(x,y)=2Δy⋅x−2Δx⋅y+2Δx⋅B=0
(注:乘以 2 是为了后续消除分母中的 0.5)
2. 中点决策逻辑
当我们已经确定了一个点 P(xi,yi)P(x_i, y_i)P(xi,yi),下一个点可能是 E(xi+1,yi)E(x_i+1, y_i)E(xi+1,yi) 或 NE(xi+1,yi+1)NE(x_i+1, y_i+1)NE(xi+1,yi+1)。
为了判断选哪个,我们取这两个点的中点 M(xi+1,yi+0.5)M(x_i+1, y_i+0.5)M(xi+1,yi+0.5):
- 如果 F(M)<0F(M) < 0F(M)<0:说明中点在直线上方,直线更靠近下面的 EEE 点。
- 如果 F(M)>0F(M) > 0F(M)>0:说明中点在直线下方,直线更靠近上面的 NENENE 点。
因此,决策参数 ddd 实际上就是 F(M)F(M)F(M) 的值。
3. 推导初始值 d0d_0d0
第一个点是起始点 (x0,y0)(x_0, y_0)(x0,y0),我们要决定第二个点,所以考察第一个中点 M0(x0+1,y0+0.5)M_0(x_0+1, y_0+0.5)M0(x0+1,y0+0.5):
将 M0M_0M0 代入隐式方程 F(x,y)F(x, y)F(x,y):
d0=F(x0+1,y0+0.5)d_0 = F(x_0+1, y_0+0.5)d0=F(x0+1,y0+0.5)
d0=2Δy(x0+1)−2Δx(y0+0.5)+2Δx⋅Bd_0 = 2\Delta y(x_0+1) - 2\Delta x(y_0+0.5) + 2\Delta x \cdot Bd0=2Δy(x0+1)−2Δx(y0+0.5)+2Δx⋅B
因为起始点 (x0,y0)(x_0, y_0)(x0,y0) 就在直线上,满足 F(x0,y0)=0F(x_0, y_0) = 0F(x0,y0)=0:
2Δy⋅x0−2Δx⋅y0+2Δx⋅B=02\Delta y \cdot x_0 - 2\Delta x \cdot y_0 + 2\Delta x \cdot B = 02Δy⋅x0−2Δx⋅y0+2Δx⋅B=0
将上述两式相减(即用 F(M0)F(M_0)F(M0) 减去 F(x0,y0)F(x_0, y_0)F(x0,y0)):
d0=2Δy(x0+1−x0)−2Δx(y0+0.5−y0)d_0 = 2\Delta y(x_0+1 - x_0) - 2\Delta x(y_0+0.5 - y_0)d0=2Δy(x0+1−x0)−2Δx(y0+0.5−y0)
d0=2Δy(1)−2Δx(0.5)d_0 = 2\Delta y(1) - 2\Delta x(0.5)d0=2Δy(1)−2Δx(0.5)
d0=2Δy−Δxd_0 = 2\Delta y - \Delta xd0=2Δy−Δx
- 循环迭代 (从 x0x_0x0 到 x1x_1x1):
-
绘制当前点 (x,y)(x, y)(x,y)。
-
如果 d<0d < 0d<0:
-
下一个点为 (x+1,y)(x+1, y)(x+1,y)(不上升)。
-
更新决策参数:dnew=d+2Δyd_{new} = d + 2\Delta ydnew=d+2Δy。
-
如果 d≥0d \ge 0d≥0:
-
下一个点为 (x+1,y+1)(x+1, y+1)(x+1,y+1)(向上走一步)。
-
更新决策参数:dnew=d+2Δy−2Δxd_{new} = d + 2\Delta y - 2\Delta xdnew=d+2Δy−2Δx。
推导 di+1d_{i+1}di+1 的递推公式,本质上是看当 xxx 增加 1 时,新的中点相对于直线的位置发生了什么变化。
我们已知决策参数 ddd 的定义是中点 MMM 代入直线方程的结果:
di=F(xi+1,yi+0.5)d_i = F(x_i + 1, y_i + 0.5)di=F(xi+1,yi+0.5)
1. 情况一:当 di<0d_i < 0di<0 时
此时直线距离下方的像素点 E(xi+1,yi)E(x_i+1, y_i)E(xi+1,yi) 更近。
- 下一个像素点选择 :(xi+1,yi+1)=(xi+1,yi)(x_{i+1}, y_{i+1}) = (x_i+1, y_i)(xi+1,yi+1)=(xi+1,yi)
- 新的中点坐标 :Mnext=(xi+2,yi+0.5)M_{next} = (x_i+2, y_i+0.5)Mnext=(xi+2,yi+0.5)
我们要计算 di+1d_{i+1}di+1:
di+1=F(xi+2,yi+0.5)d_{i+1} = F(x_i+2, y_i+0.5)di+1=F(xi+2,yi+0.5)
将其与 did_idi 相减:
di+1−di=F(xi+2,yi+0.5)−F(xi+1,yi+0.5)d_{i+1} - d_i = F(x_i+2, y_i+0.5) - F(x_i+1, y_i+0.5)di+1−di=F(xi+2,yi+0.5)−F(xi+1,yi+0.5)
代入直线方程 F(x,y)=2Δy⋅x−2Δx⋅y+CF(x,y) = 2\Delta y \cdot x - 2\Delta x \cdot y + CF(x,y)=2Δy⋅x−2Δx⋅y+C:
di+1−di=2Δy(xi+2−(xi+1))−2Δx(yi+0.5−(yi+0.5))d_{i+1} - d_i = 2\Delta y (x_i+2 - (x_i+1)) - 2\Delta x (y_i+0.5 - (y_i+0.5))di+1−di=2Δy(xi+2−(xi+1))−2Δx(yi+0.5−(yi+0.5))
di+1=di+2Δyd_{i+1} = d_i + 2\Delta ydi+1=di+2Δy
2. 情况二:当 di≥0d_i \ge 0di≥0 时
此时直线距离上方的像素点 NE(xi+1,yi+1)NE(x_i+1, y_i+1)NE(xi+1,yi+1) 更近。
- 下一个像素点选择 :(xi+1,yi+1)=(xi+1,yi+1)(x_{i+1}, y_{i+1}) = (x_i+1, y_i+1)(xi+1,yi+1)=(xi+1,yi+1)
- 新的中点坐标 :Mnext=(xi+2,yi+1.5)M_{next} = (x_i+2, y_i+1.5)Mnext=(xi+2,yi+1.5)
同样计算 di+1d_{i+1}di+1 与 did_idi 的差值:
di+1−di=F(xi+2,yi+1.5)−F(xi+1,yi+0.5)d_{i+1} - d_i = F(x_i+2, y_i+1.5) - F(x_i+1, y_i+0.5)di+1−di=F(xi+2,yi+1.5)−F(xi+1,yi+0.5)
代入方程:
di+1−di=2Δy(xi+2−(xi+1))−2Δx(yi+1.5−(yi+0.5))d_{i+1} - d_i = 2\Delta y (x_i+2 - (x_i+1)) - 2\Delta x (y_i+1.5 - (y_i+0.5))di+1−di=2Δy(xi+2−(xi+1))−2Δx(yi+1.5−(yi+0.5))
di+1−di=2Δy(1)−2Δx(1)d_{i+1} - d_i = 2\Delta y(1) - 2\Delta x(1)di+1−di=2Δy(1)−2Δx(1)
di+1=di+2Δy−2Δxd_{i+1} = d_i + 2\Delta y - 2\Delta xdi+1=di+2Δy−2Δx
3. 递推总结表
| 判定条件 | 像素移动方向 | 决策参数更新公式 |
|---|---|---|
| di<0d_i < 0di<0 | 水平向右 (x+1,yx+1, yx+1,y) | di+1=di+2Δyd_{i+1} = d_i + 2\Delta ydi+1=di+2Δy |
| di≥0d_i \ge 0di≥0 | 右上对角 (x+1,y+1x+1, y+1x+1,y+1) | di+1=di+2Δy−2Δxd_{i+1} = d_i + 2\Delta y - 2\Delta xdi+1=di+2Δy−2Δx |
3. 为什么它这么快?
在早期的硬件上,浮点数运算(y=mx+by = mx + by=mx+b 中的乘法和加法)非常吃资源。Bresenham 的天才之处在于:
- 全是整数 :通过将方程两边同乘以 Δx\Delta xΔx,消除了分母。
- 增量计算:每一步的结果都基于上一步,不需要重新计算复杂的公式。
- 位移优化 :乘以 2 的操作在计算机底层只需向左移一位(
<< 1),速度极快。
4. 扩展
上述步骤仅适用于第一象限且斜率较平缓的情况。在实际应用中,我们需要处理以下变体:
- 斜率大于 1 :交换 xxx 和 yyy 的逻辑,以 yyy 为步进方向。
- 负斜率 :调整步进的方向(变为 −1-1−1)。
- 通用版:现代库(如 OpenCV 或底层驱动)通常会集成一个包含绝对值和符号判断的通用 Bresenham 逻辑。
代码实现
c
void rst::rasterizer::draw_line(Eigen::Vector3f begin, Eigen::Vector3f end)
{
auto x1 = begin.x(); auto y1 = begin.y();
auto x2 = end.x(); auto y2 = end.y();
Eigen::Vector3f line_color = {255, 255, 255}; // 直线颜色为白色
// 算法核心变量:dx/dy是坐标差,dx1/dy1是绝对值,px/py是误差项
int x,y,dx,dy,dx1,dy1,px,py,xe,ye,i;
dx=x2-x1; dy=y2-y1;
dx1=fabs(dx); dy1=fabs(dy);
px=2*dy1-dx1; // 初始误差项(x为主方向时)
py=2*dx1-dy1; // 初始误差项(y为主方向时)
// 情况1:x方向变化更大(dx1 >= dy1),以x为步进方向
if(dy1<=dx1)
{
// 确定绘制的起点和终点(保证x递增,简化循环)
if(dx>=0) { x=x1; y=y1; xe=x2; }
else { x=x2; y=y2; xe=x1; }
set_pixel(Eigen::Vector3f(x, y, 1.0f), line_color); // 绘制起点像素
for(i=0;x<xe;i++)
{
x++; // x步进1
if(px<0) { px += 2*dy1; } // 误差项不足,仅x步进
else {
// 误差项足够,y步进(根据斜率正负决定上下)
y += ((dx<0 && dy<0) || (dx>0 && dy>0)) ? 1 : -1;
px += 2*(dy1-dx1);
}
set_pixel(Eigen::Vector3f(x, y, 1.0f), line_color); // 绘制当前像素
}
}
// 情况2:y方向变化更大(dy1 > dx1),以y为步进方向(逻辑和x为主方向对称)
else
{
if(dy>=0) { x=x1; y=y1; ye=y2; }
else { x=x2; y=y2; ye=y1; }
set_pixel(Eigen::Vector3f(x, y, 1.0f), line_color);
for(i=0;y<ye;i++)
{
y++;
if(py<=0) { py += 2*dx1; }
else {
x += ((dx<0 && dy<0) || (dx>0 && dy>0)) ? 1 : -1;
py += 2*(dx1-dy1);
}
set_pixel(Eigen::Vector3f(x, y, 1.0f), line_color);
}
}
}