一、经典 Bresenham 算法讲解
bresenham 在提出离散直线绘制方式时,主要针对斜率在 0~1 之间的直线,那么我们就先在这个区域进行解析具体是如何绘制的。因此本章核心的一个要点是分析所谓的 err 是怎么来的。【一个网格表示一个像素点,它的坐标用左下点进行表示】
(一)一条直线在网格中是怎么走?

如上图,我们从起始点开始出发,当在 x 方向发生步进 2 个单位时,存在两个候选点(蓝色和橙色),该选谁?那么很自然的,我们肯定要看目标直线与 x = x0 + 2 之间的交点(处于网格的左侧线段),它是偏上的还是偏下的(以 0.5 作为依据),很显然,它是偏上的,所以我们选择蓝色的候选点。
以此类推,我们将可以快速绘制整条直线。

(二)误差项推导
在正式推导之前,或许可以先思考一个问题:选取候选点的本质逻辑是什么?
从上面的讨论中,这个问题也很好回答,即“以起始点为参考,判断当前网格的左侧交点对应的小数部分是否大于 0.5 即可”。
所以,如何描述当前网格交点的小数部分是解决问题的关键!其实确定一个网格无非就是确定在 x 方向的整数值和 y 方向的整数值。现已知是在 x 方向上步进的,所以考虑当步进了 i 个单位后,在 y 方向的变化是多少?
假设直线方程:
y=ΔyΔxx+b y = \frac{\Delta y}{\Delta x}x + b y=ΔxΔyx+b
这里的斜率直接用变量去表示,应该很好理解。变形:
Δx⋅(y)=Δy⋅(x)+bΔx \Delta x \cdot (y) = \Delta y \cdot (x) + b \Delta x Δx⋅(y)=Δy⋅(x)+bΔx
当相对于起始点步进 i 个单位时:
Δx⋅(y0+M)=Δy⋅(x0+i)+bΔx \Delta x \cdot (y_0 + M) = \Delta y \cdot (x_0 + i) + b \Delta x Δx⋅(y0+M)=Δy⋅(x0+i)+bΔx
其中,M 是相对于起始点在 y 轴上的偏移量,通过简单的求解,我们可以得到:
M=i⋅ΔyΔx=整数部分+小数部分=ycurrent+pointY M = i \cdot \frac{\Delta y}{\Delta x} = \text{整数部分} + \text{小数部分} = y_\text{current} + pointY M=i⋅ΔxΔy=整数部分+小数部分=ycurrent+pointY
别忘了我们的目的是获取当前网格的小数部分,所以:
pointY=i⋅ΔyΔx−ycurrent pointY = i \cdot \frac{\Delta y}{\Delta x} - y_\text{current} pointY=i⋅ΔxΔy−ycurrent
所以当 x 步进 i 步时,就看 pointY 和 0.5 之间的比较来决定是:
- yi=yi−1+1y_i = y_{i-1} + 1yi=yi−1+1 还是
- yi=yi−1y_i = y_{i-1}yi=yi−1
所以:
(i⋅ΔyΔx−ycurrent>0.5) ? (yi=yi−1+1):(yi=yi−1) (i \cdot \frac{\Delta y}{\Delta x} - y_\text{current} > 0.5) \ ?\ (y_i = y_{i-1} + 1):(y_i = y_{i-1}) (i⋅ΔxΔy−ycurrent>0.5) ? (yi=yi−1+1):(yi=yi−1)
考虑到浮点数的计算比较慢且会产生误差,所以对不等式进行两边同时乘上 2Δx2\Delta x2Δx,得:
(i∗2Δy−2Δx∗ycurrent−Δx>0) ? (yi=yi−1+1):(yi=yi−1) (i * 2 \Delta y - 2 \Delta x * y_\text{current} - \Delta x > 0) \ ?\ (y_i = y_{i-1} + 1):(y_i = y_{i-1}) (i∗2Δy−2Δx∗ycurrent−Δx>0) ? (yi=yi−1+1):(yi=yi−1)
所以我们就记误差项是:
err=i∗2Δy−2Δx∗ycurrent−Δx err = i * 2 \Delta y - 2 \Delta x * y_\text{current} - \Delta x err=i∗2Δy−2Δx∗ycurrent−Δx
由于我们是逐个像素进行步进的,所以需要记录初始误差项,也就是 i = 1 时的表达式(此时 ycurrent=0y_\text{current} = 0ycurrent=0,因为我们是相对于起始点进行步进而不是原点):
err0=2Δy−Δx err_0 = 2 \Delta y - \Delta x err0=2Δy−Δx
这就是大家常见的 err 的由来了!,是不是很简单,那么恭喜你,你已经掌握标准的 bresenham 算法 了,下面只是进行优化而已。
优化逻辑:考虑到每次都是在 x 步进一个单位,那么我们希望找到每次步进时,误差项的增量是多少(缓存方式可以提高运算效率)?
Δerr=erri+1−erri={2Δy,x 在第 i次步进时,y也步进2Δy−2Δx,x 在第 i次步进时,y不步进 \Delta err=err_{i+1} - err_i = \begin{cases} 2\Delta y, & \text{x 在第 } i 次步进时,y 也步进 \\ 2\Delta y - 2\Delta x, & \text{x 在第 } i 次步进时,y 不步进 \\ \end{cases} Δerr=erri+1−erri={2Δy,2Δy−2Δx,x 在第 i次步进时,y也步进x 在第 i次步进时,y不步进
从上面的公式可以得出结论:
当 x 每步进一次时,误差项总是加上 2Δy2\Delta y2Δy,若是 y 也发生步进,则误差项额外减去 2Δx2\Delta x2Δx。
可以理解为:这是一种y轴步进的惩罚机制,确保在y方向上呈现周期性步进(阶梯式步进)(其实是可以求出周期的,但是这里先不讲)。
(三)C++ 实现代码参考
下面是一个使用 C++ 实现的 Bresenham 直线绘制算法示例代码,适用于斜率在 ( 0 \sim 1 ) 之间的情形:
#include <iostream>
using namespace std;
// 定义一个绘制函数
void bresenham(int x0, int y0, int x1, int y1) // 输入起始坐标和终点坐标
{
// 当前坐标,进行实时输出
int x = x0;
int y = y0;
// 计算变化量
int dx = x1 - x0;
int dy = y1 - y0;
// 计算初始误差项
int err = 2 * dy - dx;
// 打印起始点,因为步进过程和起始点无关(i > 0)
cout << "(" << x << "," << y << ")" << endl;
// 开始步进
for (int i = 1; i <= dx; i++) // 例如 dx = 2,i = 1, i = 2
{
x++;
if (err > 0) // 判断副轴是否发生步进
{
y++;
err -= 2 * dx; // 发生步进则误差项额外减去 2 * dx
}
err += 2 * dy; // 每步进一次都要加上 2 * dy
// 输出步进过程的点
cout << "(" << x << "," << y << ")" << endl;
}
}
int main()
{
bresenham(0, 0, 20, 10); // 斜率在 0~1 之间
return 0;
}
二、将 Bresenham 算法拓展到全平面
(一)直线在整个平面怎么描述?
我们不妨将平面划分成 8 个区域:

我们之前已经讨论一区的情况,那现在不妨试着讨论二区的情况:
同样地道理,在斜率处于二区中的直线,它们是以 +y 轴(这里最好加上正负,表示半轴)进行步进的,所以直线的方程可以写成:
x=ΔxΔyy+b x = \frac{\Delta x}{\Delta y} y + b x=ΔyΔxy+b

由于我们在二区中是以 +y 半轴方向为主方向进行步进的,所以考虑直线与当前网格的交点是基于网格的下侧线段,所以按照同样地方式,我们可以得到当 +y 方向从起始点步进 i 个单位时,副方向 +x 相对于起始点发生的变化是:
M=i∗ΔxΔy=整数部分+小数部分=xcurrent+pointX M = i * \frac{\Delta x}{\Delta y} = \text{整数部分} + \text{小数部分} = x_\text{current} + pointX M=i∗ΔyΔx=整数部分+小数部分=xcurrent+pointX
所以小数部分:
pointX=i∗ΔxΔy−xcurrent pointX = i * \frac{\Delta x}{\Delta y} - x_\text{current} pointX=i∗ΔyΔx−xcurrent
整数化的判断条件得:
(i∗2Δx−2Δy∗xcurrent−Δy>0)? (xi=xi−1+1):(xi=xi−1) (i * 2 \Delta x - 2 \Delta y * x_\text{current} - \Delta y > 0)?\ (x_i = x_{i-1} + 1):(x_i = x_{i-1}) (i∗2Δx−2Δy∗xcurrent−Δy>0)? (xi=xi−1+1):(xi=xi−1)
所以初始误差项为:
err0=2Δx−Δy err_0 = 2 \Delta x - \Delta y err0=2Δx−Δy
误差项的增量是:
Δerr=erri+1−erri={2Δx,+y 在第 i 次时,+x 也步进2Δx−2Δy,+y 在第 i 次时,+x 不步进 \Delta err=err_{i+1} - err_i = \begin{cases} 2\Delta x, & +y \text{ 在第 } i \text{ 次时,+x 也步进} \\ 2\Delta x - 2\Delta y, & +y \text{ 在第 } i \text{ 次时,+x 不步进} \\ \end{cases} Δerr=erri+1−erri={2Δx,2Δx−2Δy,+y 在第 i 次时,+x 也步进+y 在第 i 次时,+x 不步进
从上面的公式可以得出结论:
当主方向 +y 每步进一次时,误差项总是加上 2Δx2\Delta x2Δx,若是副方向 +x 也发生步进,则误差项额外减去 2Δy2\Delta y2Δy。
(二)一般化处理得到全平面绘制方法
首先,步进方向一共分成 4 个方向,即 +x、+y、-x、-y,主/副方向步进 i 个单位表示为:
主/副变量 = 主变量 + i,其中 i = ±1;
我们可以作一下的判断:
还需要非常关注的一个点是:xcurrentx_\text{current}xcurrent 与 ycurrenty_\text{current}ycurrent 它是具有正负性的;在负半轴区域,判断表达式我们是以 -0.5 作为分界。这两个注意点是由于:
直线是在正负方向进行延伸产生不一样的变化过程。
关于直线与当前网格的交点,要取哪个线段的交点也有讲究:
- 当主方向为 +x 时,取 左侧线段
- +y 时,取 下侧线段
- -x 时,取 右侧线段
- -y 时,取 上侧线段
其实上述提到的这些小注意事项,各位小伙伴可以在图上进行画一下就可以理解了,很简单的。
当时我在分析的时候,将所有的区域都一一分析,最后进行提炼归类,然后发现存在完美的对称性与一致性,那现在我们就开始吧!
我们对于任意的直线,有如下结论:
任意方向直线的分类与判断条件(带有实际含义):
我们可以对直线按主方向归类,并结合所在区域给出判断条件:

注:这里的所有变量在不使用 || (绝对值符号)时均表示实际的含义。
带有实际含义的整数化:

带如果现在使用绝对值去表示,将化简为(但是ix与iy不进行绝对值处理):

(三)最终全局通用版 Bresenham 代码实现
由于我们前面已经对各个方向进行了统一的误差项分析与增量逻辑归纳,因此可以很容易地写出一份适用于全平面的通用绘制函数。
只需要先判断主方向是 X 还是 Y,分别使用对应的误差项公式进行步进更新即可。
✅ 通用实现代码(C++):
#include <iostream>
using namespace std;
// 定义一个绘制函数
void bresenham(int x0, int y0, int x1, int y1) // 输入起始坐标和终点坐标
{
// 当前坐标,进行实时输出
int x = x0;
int y = y0;
// 计算变化量
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
// 计算步进系数
int ix = (x0 < x1) ? 1 : -1;
int iy = (y0 < y1) ? 1 : -1;
// 计算初始误差项
int errX = 2 * dy - dx; // 主 X 方向
int errY = 2 * dx - dy; // 主 Y 方向
// 打印起始点
cout << "(" << x << "," << y << ")" << endl;
// 开始步进(判断主方向)
if (dx >= dy) // 主 X
{
for (int i = 1; i <= dx; i++)
{
x += ix;
if (errX > 0)
{
y += iy;
errX -= 2 * dx;
}
errX += 2 * dy;
cout << "(" << x << "," << y << ")" << endl;
}
}
else // 主 Y
{
for (int i = 1; i <= dy; i++)
{
y += iy;
if (errY > 0)
{
x += ix;
errY -= 2 * dy;
}
errY += 2 * dx;
cout << "(" << x << "," << y << ")" << endl;
}
}
}
int main()
{
cout << "一区测试:" << endl;
bresenham(0, 0, 10, 5);
cout << "二区测试:" << endl;
bresenham(0, 0, 5, 10);
cout << "三区测试:" << endl;
bresenham(0, 0, -5, 10);
cout << "四区测试:" << endl;
bresenham(0, 0, -10, 5);
cout << "五区测试:" << endl;
bresenham(0, 0, -10, -5);
cout << "六区测试:" << endl;
bresenham(0, 0, -5, -10);
cout << "七区测试:" << endl;
bresenham(0, 0, 5, -10);
cout << "八区测试:" << endl;
bresenham(0, 0, 10, -5);
return 0;
}
三、将 Bresenham 算法拓展到三维空间
设空间直线方程为:
x−x0a=y−y0b=z−z0c \frac{x - x_0}{a} = \frac{y - y_0}{b} = \frac{z - z_0}{c} ax−x0=by−y0=cz−z0
如果将空间进行网格化,那么应当以平面的方式进行推进,直线的落点在网格体的一个侧面上。
我们不妨对由 +x、+y、+z 所组成的子空间进行分析,如图所示:

我们可以先研究第一个子正四面体空间,并且主步进方向是 +X。 下面我们观察该正四面体的底面 ABCD(往 +x 方向延伸的正方形平面),并从原点的角度进行观察该平面:

现在,我们需要考虑每次步进有几个候选点。由于现在已知每次步进可以认为是一个平面的推进,又因为我们已经划分好 24 个区域,所以我们不妨取区域 I 来分析。
即主方向是 +x,副方向是 +y 和 +z,假设直线在该推进平面的某个网格中的落点为 P 点,如下:

那么,我们完全可以通过投影的方式分别将 P 投影到 xoy 平面 和 yoz 平面, 候选点是可以通过先独立判断再进行合并的方式来选取(本例中选择灰色的像素点)。
那么投影后不就是之前所讨论的二维 Bresenham 嘛,也就是说:
三维空间中存在两个副方向、一个主方向,主方向每步进一个单位,可以单独判断两个副方向是否步进。
所以我们可以直接写出初始项的表达式以及它的增量。
🔹 初差初始项计算:
- 主 X 方向:
副 Y:err0=2∣Δy∣−∣Δx∣>0副 Z:err0=2∣Δz∣−∣Δx∣>0 \text{副 Y:} \quad err0 = 2|\Delta y| - |\Delta x| > 0 \\ \text{副 Z:} \quad err0 = 2|\Delta z| - |\Delta x| > 0 副 Y:err0=2∣Δy∣−∣Δx∣>0副 Z:err0=2∣Δz∣−∣Δx∣>0
- 主 Y 方向:
副 X:err0=2∣Δx∣−∣Δy∣>0副 Z:err0=2∣Δz∣−∣Δy∣>0 \text{副 X:} \quad err0 = 2|\Delta x| - |\Delta y| > 0 \\ \text{副 Z:} \quad err0 = 2|\Delta z| - |\Delta y| > 0 副 X:err0=2∣Δx∣−∣Δy∣>0副 Z:err0=2∣Δz∣−∣Δy∣>0
- 主 Z 方向:
副 X:err0=2∣Δx∣−∣Δz∣>0副 Y:err0=2∣Δy∣−∣Δz∣>0 \text{副 X:} \quad err0 = 2|\Delta x| - |\Delta z| > 0 \\ \text{副 Y:} \quad err0 = 2|\Delta y| - |\Delta z| > 0 副 X:err0=2∣Δx∣−∣Δz∣>0副 Y:err0=2∣Δy∣−∣Δz∣>0
🔹 增量计算:
- 主 X 方向:
副 Y:Δerr={2∣Δy∣2∣Δy∣−2∣Δx∣副 Z:Δerr={2∣Δz∣2∣Δz∣−2∣Δx∣ \text{副 Y:} \quad \Delta err = \begin{cases} 2|\Delta y| \\ 2|\Delta y| - 2|\Delta x| \end{cases} \quad \text{副 Z:} \quad \Delta err = \begin{cases} 2|\Delta z| \\ 2|\Delta z| - 2|\Delta x| \end{cases} 副 Y:Δerr={2∣Δy∣2∣Δy∣−2∣Δx∣副 Z:Δerr={2∣Δz∣2∣Δz∣−2∣Δx∣
- 主 Y 方向:
副 X:Δerr={2∣Δx∣2∣Δx∣−2∣Δy∣副 Z:Δerr={2∣Δz∣2∣Δz∣−2∣Δy∣ \text{副 X:} \quad \Delta err = \begin{cases} 2|\Delta x| \\ 2|\Delta x| - 2|\Delta y| \end{cases} \quad \text{副 Z:} \quad \Delta err = \begin{cases} 2|\Delta z| \\ 2|\Delta z| - 2|\Delta y| \end{cases} 副 X:Δerr={2∣Δx∣2∣Δx∣−2∣Δy∣副 Z:Δerr={2∣Δz∣2∣Δz∣−2∣Δy∣
- 主 Z 方向:
副 X:Δerr={2∣Δx∣2∣Δx∣−2∣Δz∣副 Y:Δerr={2∣Δy∣2∣Δy∣−2∣Δz∣ \text{副 X:} \quad \Delta err = \begin{cases} 2|\Delta x| \\ 2|\Delta x| - 2|\Delta z| \end{cases} \quad \text{副 Y:} \quad \Delta err = \begin{cases} 2|\Delta y| \\ 2|\Delta y| - 2|\Delta z| \end{cases} 副 X:Δerr={2∣Δx∣2∣Δx∣−2∣Δz∣副 Y:Δerr={2∣Δy∣2∣Δy∣−2∣Δz∣
✅ 逻辑解释:每次主方向步进 1 单位时,两条副方向误差项分别累加,若误差大于 0 就同时步进副方向坐标,并修正误差项。
四、将 Bresenham 算法拓展到 N 维空间
根据三维空间中的介绍,直线可以通过投影进行独立判断,所以对于任意的维度的直线,我们都可以进行投影到每个平面进行分析。
所以对于 N 维空间,会有一个主轴和 (N-1) 个副轴,并且主轴每步进一个单位,所有副轴都可以进行独立的误差项判断,看自己对应的分量是否发生变化即可。
✅ 初始误差与增量通式表达:
副轴初始误差项:
err0=2∣Δ副∣−∣Δ主∣ err_0 = 2|\Delta_\text{副}| - |\Delta_\text{主}| err0=2∣Δ副∣−∣Δ主∣
副轴增量(每次主轴步进时):
Δerr={2∣Δ副∣,副轴不步进2∣Δ副∣−2∣Δ主∣,副轴步进 \Delta err = \begin{cases} 2|\Delta_\text{副}|, & \text{副轴不步进} \\ 2|\Delta_\text{副}| - 2|\Delta_\text{主}|, & \text{副轴步进} \end{cases} Δerr={2∣Δ副∣,2∣Δ副∣−2∣Δ主∣,副轴不步进副轴步进
✅ N 维空间通用 Bresenham 实现(C++ 模板)
#include <iostream>
#include <array>
#include <cmath>
using namespace std;
// 定义一个打印函数
template<int N>
void Draw(const array<int, N>& point)
{
cout << "(";
for (int i = 0; i < N; ++i)
{
cout << point[i];
if (i < N - 1) cout << ", ";
}
cout << ")" << endl;
}
// 定义一个绘制模板函数
template<int N>
void bresenham(array<int, N> start, array<int, N> end)
{
array<int, N> delta, step, current = start;
array<int, N> err = {0};
int main_axis = 0;
// 确定主轴与步进方向
for (int i = 0; i < N; ++i)
{
delta[i] = abs(end[i] - start[i]);
step[i] = end[i] > start[i] ? 1 : -1;
if (delta[i] > delta[main_axis]) main_axis = i;
}
// 初始化副轴误差项
for (int i = 0; i < N; ++i)
{
if (i == main_axis) continue;
err[i] = 2 * delta[i] - delta[main_axis];
}
// 初始点输出
Draw(current);
// 步进主轴
for (int i = 1; i <= delta[main_axis]; ++i)
{
current[main_axis] += step[main_axis];
for (int j = 0; j < N; ++j)
{
if (j == main_axis) continue;
if (err[j] > 0)
{
current[j] += step[j];
err[j] -= 2 * delta[main_axis];
}
err[j] += 2 * delta[j];
}
Draw(current);
}
}
int main()
{
cout << "二维测试:" << endl;
bresenham<2>({0, 0}, {5, 10}); // 2D 示例
return 0;
}
📌 本文系原创教程,所有内容包括公式推导、图示逻辑、代码实现均由作者亲自整理与编写,仅供学习与交流使用。
欢迎转载与引用,但请注明出处,尊重原创劳动。
作者:拾画cv
原文地址:https://blog.csdn.net/m0_68307574/article/details/147910203?sharetype=blogdetail&sharerId=147910203&sharerefer=PC&sharesource=m0_68307574&spm=1011.2480.3001.8118
如有问题或建议,欢迎留言交流~

519

被折叠的 条评论
为什么被折叠?



