Bresenham’s line algorithm
Task:给定两点,在位图中绘制线条。
思路起点(二维线段参数方程)
线段的参数方程
设
线
段
两
端
点
为
A
(
x
1
,
y
1
)
和
B
(
x
2
,
y
2
)
,
则
该
线
段
的
参
数
方
程
可
以
写
为
{
x
=
x
1
+
t
(
x
2
−
x
1
)
,
y
=
y
1
+
t
(
y
2
−
y
1
)
,
t
∈
[
0
,
1
]
.
其
中
,
当
t
=
0
时
,
点
在
A
;
当
t
=
1
时
,
点
在
B
;
而
t
取
[
0
,
1
]
内
的
值
时
,
表
示
A
到
B
之
间
的
所
有
点
。
对
于
三
维
空
间
中
的
线
段
,
若
端
点
为
A
(
x
1
,
y
1
,
z
1
)
和
B
(
x
2
,
y
2
,
z
2
)
,
参
数
方
程
扩
展
为
{
x
=
x
1
+
t
(
x
2
−
x
1
)
,
y
=
y
1
+
t
(
y
2
−
y
1
)
,
z
=
z
1
+
t
(
z
2
−
z
1
)
,
t
∈
[
0
,
1
]
.
设线段两端点为 A(x_1, y_1) 和 B(x_2, y_2) ,则该线段的参数方程可以写为 \\ \begin{cases} x = x_1 + t(x_2 - x_1), \\ y = y_1 + t(y_2 - y_1), \end{cases} \quad t \in [0, 1]. \\ 其中,当 t = 0 时,点在 A ;当 t = 1 时,点在 B ;\\而 t 取 [0,1] 内的值时,表示 A 到 B 之间的所有点。 \\ 对于三维空间中的线段,若端点为 A(x_1, y_1, z_1) 和 B(x_2, y_2, z_2) ,参数方程扩展为 \\ \begin{cases} x = x_1 + t(x_2 - x_1), \\ y = y_1 + t(y_2 - y_1), \\ z = z_1 + t(z_2 - z_1), \end{cases} \quad t \in [0, 1]. \\
设线段两端点为A(x1,y1)和B(x2,y2),则该线段的参数方程可以写为{x=x1+t(x2−x1),y=y1+t(y2−y1),t∈[0,1].其中,当t=0时,点在A;当t=1时,点在B;而t取[0,1]内的值时,表示A到B之间的所有点。对于三维空间中的线段,若端点为A(x1,y1,z1)和B(x2,y2,z2),参数方程扩展为⎩⎪⎨⎪⎧x=x1+t(x2−x1),y=y1+t(y2−y1),z=z1+t(z2−z1),t∈[0,1].
这种参数方程表示方法简洁直观,常用于几何、物理和计算机图形学等领域。
自定义绘制精度
代码实现
void DrawLine(const int ax, const int ay, const int bx, const int by, const Vec3 &color)
{
for (float t = 0.; t < 1.; t += .002)
{
int x = std::round(ax + (bx - ax) * t);
int y = std::round(ay + (by - ay) * t);
SetColor(x, y, color);
}
}
存在问题
当t定义的精度不满足绘制线段长度时(采样率不够),线段会出现”间隙“。固定的精度没法适应多样的绘制情况,如果t值精度根据屏幕分辨率设置为最大情况会浪费性能。
尝试优化:线段长度适应性精度(欧氏距离)
由于前述问题,下面是根据两点长度调整精度的思路实现。
代码实现
void DrawLine(const int ax, const int ay, const int bx, const int by, const Vec3 &color)
{
// 基于线段距离采样的画线算法
const float d = sqrtf((bx - ax) * (bx - ax) + (by - ay) * (by - ay));
const float h = 1.0f / d;
const int cnt = (int)(d + 0.5f);
const float stepX = (bx - ax) * h;
const float stepY = (by - ay) * h;
for (int t = 0; t < cnt; t++)
{
int x = (int)(ax + t * stepX + 0.5f);
int y = (int)(ay + t * stepY + 0.5f);
SetColor(x, y, color);
}
}
存在问题
效果勉强,性能欠佳。一是最长的线段由于浮点乘法运算累积的误查导致”锯齿“明显。二是循环中存在浮点乘法运算。
尝试消除浮点乘法运算,以细粒度控制局部误差。
直接来看Bresenham's line algorithm
是怎么做的。
Bresenham画线算法
基于整数增量误差原理消除浮点运算。Bresenham 算法的核心是通过整数运算和误差累积,选择最接近理想直线的像素点。
数学推导
以直线斜率 ( 0 ≤ m ≤ 1 )为例 。
问题定义
给
定
起
点
(
x
0
,
y
0
)
和
终
点
(
x
1
,
y
1
)
,
其
中
(
x
0
,
y
0
,
x
1
,
y
1
∈
Z
∗
)
绘
制
一
条
直
线
,
满
足
Δ
x
=
x
1
−
x
0
>
0
,
且
斜
率
m
=
Δ
y
Δ
x
,
其
中
(
0
≤
m
≤
1
)
。
给定起点 (x_0, y_0) 和终点 (x_1, y_1) ,其中(x_0,y_0,x_1,y_1∈Z_*)\\绘制一条直线,满足 \Delta x = x_1 - x_0 > 0 ,且斜率 m = \frac{\Delta y}{\Delta x} ,其中 ( 0 ≤ m ≤ 1 )。
给定起点(x0,y0)和终点(x1,y1),其中(x0,y0,x1,y1∈Z∗)绘制一条直线,满足Δx=x1−x0>0,且斜率m=ΔxΔy,其中(0≤m≤1)。
定义理想直线方程
y
=
m
(
x
−
x
0
)
+
y
0
当
x
=
x
k
+
1
时
,
理
想
y
坐
标
为
:
y
ideal
=
m
(
x
k
+
1
−
x
0
)
+
y
0
y = m(x - x_0) + y_0 \\ 当 x = x_k + 1 时,理想 y 坐标为: \\ y_{\text{ideal}} = m(x_k + 1 - x_0) + y_0
y=m(x−x0)+y0当x=xk+1时,理想y坐标为:yideal=m(xk+1−x0)+y0
定义当前像素与候选像素
假
设
当
前
绘
制
的
像
素
为
(
x
k
,
y
k
)
,
下
一
步
可
能
的
候
选
像
素
为
:
候选像素
A
:
(
x
k
+
1
,
y
k
)
(
不
增
加
y
)
候选像素
B
:
(
x
k
+
1
,
y
k
+
1
)
(
增
加
y
)
假设当前绘制的像素为 (x_k, y_k) ,下一步可能的候选像素为:\\ \textbf{候选像素}A:(x_k + 1, y_k) (不增加 y ) \\ \textbf{候选像素}B: (x_k + 1, y_k + 1) (增加 y )
假设当前绘制的像素为(xk,yk),下一步可能的候选像素为:候选像素A:(xk+1,yk)(不增加y)候选像素B:(xk+1,yk+1)(增加y)
任务决策转化
这样,我们的问题变成:从起点开始,每次沿 x 方向步进 1 单位,确定是否需要沿 y 方向步进 1 单位。
那么如何做决策呢,谁离理想直线更近就选谁,引入误差量化二者分别的接近程度。
定义误差项
利
用
二
分
思
想
定
义
候
选
像
素
A
和
B
的
中
点
为
C
(
x
k
+
1
,
y
k
+
0.5
)
。
定
义
误
差
项
e
k
=
y
ideal
−
(
y
k
+
0.5
)
,
表
示
理
想
点
与
C
的
垂
直
距
离
关
系
。
根
据
e
k
的
符
号
,
我
们
可
以
判
断
理
想
点
离
A
,
B
中
的
谁
更
近
。
不
妨
乘
以
2
Δ
x
消
除
浮
点
运
算
及
分
母
,
整
理
后
可
以
得
到
:
e
k
=
2
Δ
x
⋅
(
y
ideal
−
y
k
−
0.5
)
根
据
误
差
项
e
k
,
决
定
选
择
候
选
像
素
A
或
B
:
若
e
k
<
0
:
选
择
像
素
A
(
不
增
加
y
)
。
若
e
k
≥
0
:
选
择
像
素
B
(
增
加
y
)
。
利用二分思想定义候选像素A和B的中点为C (x_k + 1, y_k + 0.5) 。\\ 定义误差项 e_k= y_{\text{ideal}} - (y_k+0.5),表示理想点与C的垂直距离关系。 \\ 根据e_k的符号,我们可以判断理想点离A,B中的谁更近。\\ 不妨乘以2 \Delta x消除浮点运算及分母,整理后可以得到:\\ e_k = 2 \Delta x \cdot \left( y_{\text{ideal}} - y_k - 0.5 \right) \\ \\根据误差项 e_k ,决定选择候选像素A或B: \\ 若 e_k < 0 :选择像素A(不增加 y )。\\ 若 e_k \geq 0 :选择像素B(增加 y )。
利用二分思想定义候选像素A和B的中点为C(xk+1,yk+0.5)。定义误差项ek=yideal−(yk+0.5),表示理想点与C的垂直距离关系。根据ek的符号,我们可以判断理想点离A,B中的谁更近。不妨乘以2Δx消除浮点运算及分母,整理后可以得到:ek=2Δx⋅(yideal−yk−0.5)根据误差项ek,决定选择候选像素A或B:若ek<0:选择像素A(不增加y)。若ek≥0:选择像素B(增加y)。
误差项递推公式
根
据
第
k
次
选
择
不
同
得
到
误
差
递
推
关
系
:
若
选
择
像
素
A
(
不
增
加
y
)
:
e
k
+
1
=
2
Δ
x
⋅
(
y
ideal
(
k
+
1
)
−
y
k
−
0.5
)
由
于
y
ideal
(
k
+
1
)
=
y
ideal
(
k
)
+
m
,
代
入
后
化
简
得
:
e
k
+
1
=
e
k
+
2
Δ
y
若
选
择
像
素
B
(
增
加
y
)
:
e
k
+
1
=
2
Δ
x
⋅
(
y
ideal
(
k
+
1
)
−
(
y
k
+
1
)
−
0.5
)
化
简
后
得
:
e
k
+
1
=
e
k
+
2
Δ
y
−
2
Δ
x
根据第k次选择不同得到误差递推关系:\\ 若选择像素A(不增加 y ): e_{k+1} = 2 \Delta x \cdot \left( y_{\text{ideal} (k+1)} - y_{k} - 0.5 \right) \\ 由于 y_{\text{ideal} (k+1)} = y_{\text{ideal}(k)} + m ,代入后化简得: e_{k+1} = e_k + 2 \Delta y \\ 若选择像素B(增加 y ): e_{k+1} = 2 \Delta x \cdot \left( y_{\text{ideal}(k+1)} - (y_k + 1) - 0.5 \right) \\ 化简后得: e_{k+1} = e_k + 2 \Delta y - 2 \Delta x
根据第k次选择不同得到误差递推关系:若选择像素A(不增加y):ek+1=2Δx⋅(yideal(k+1)−yk−0.5)由于yideal(k+1)=yideal(k)+m,代入后化简得:ek+1=ek+2Δy若选择像素B(增加y):ek+1=2Δx⋅(yideal(k+1)−(yk+1)−0.5)化简后得:ek+1=ek+2Δy−2Δx
误差项迭代规则
-
选择像素A(不增加 y ):
e k + 1 = e k + 2 Δ y e_{k+1} = e_k + 2 \Delta y ek+1=ek+2Δy -
选择像素B(增加 y ):
e k + 1 = e k + 2 Δ y − 2 Δ x e_{k+1} = e_k + 2 \Delta y - 2 \Delta x ek+1=ek+2Δy−2Δx
初始误差项
初
始
时
x
=
x
0
,
理
想
直
线
在
y
=
y
0
,
此
时
:
{
e
1
=
2
Δ
x
⋅
(
y
ideal
(
1
)
−
y
0
−
0.5
)
,
y
ideal
(
1
)
=
y
ideal
(
0
)
+
m
,
y
ideal
(
0
)
=
y
0
⇒
e
1
=
2
Δ
y
−
Δ
x
初始时 x = x_0 ,理想直线在 y = y_0 ,此时: \\ \left\{\begin{matrix} e_1 = 2 \Delta x \cdot \left( y_{\text{ideal} (1)} - y_{0} - 0.5 \right), \\ y_{\text{ideal} (1)} = y_{\text{ideal}(0)} + m, \\ y_{\text{ideal}(0)}=y_0 \end{matrix}\right. \Rightarrow e_1 = 2 \Delta y - \Delta x
初始时x=x0,理想直线在y=y0,此时:⎩⎨⎧e1=2Δx⋅(yideal(1)−y0−0.5),yideal(1)=yideal(0)+m,yideal(0)=y0⇒e1=2Δy−Δx
扩展到所有八分圆
上 述 推 导 假 设 0 ≤ m ≤ 1 且 Δ x > 0 。 对 于 其 他 情 况 : 上述推导假设 0 ≤ m ≤ 1 且 \Delta x > 0 。对于其他情况: 上述推导假设0≤m≤1且Δx>0。对于其他情况:
斜率 m > 1 : 交 换 x 和 y 的 角 色 , 沿 y 方 向 步 进 。 负斜率 : 调 整 步 进 方 向 为 − 1 。 反向绘制 ( Δ x < 0 ) : 交 换 起 点 和 终 点 。 \textbf{斜率} m > 1 :交换 x 和 y 的角色,沿 y 方向步进。 \\ \textbf{负斜率}:调整步进方向为 -1 。 \\ \textbf{反向绘制}( \Delta x < 0 ):交换起点和终点。 斜率m>1:交换x和y的角色,沿y方向步进。负斜率:调整步进方向为−1。反向绘制(Δx<0):交换起点和终点。
思路要点总结
- 误差项 ( e ):二分思想量化理想直线与候选像素的位置关系。
- 整数运算:通过乘以常数
2△x
消除分母,全程无浮点运算。 - 动态调整:误差项累积和修正确保路径始终最接近理想直线。
代码实现
// Bresenham's line algorithm
void DrawLine(int ax, int ay, int bx, int by, const Vec3 &color)
{
int dx = std::abs(bx - ax);
int dy = std::abs(by - ay);
bool steep = false;
// 斜率绝对值是否大于1
if (dx < dy)
{
std::swap(ax, ay);
std::swap(bx, by);
std::swap(dx, dy);
steep = true;
}
// deltaX符号
if (bx - ax < 0)
{
std::swap(ax, bx);
std::swap(ay, by);
}
const int stepY = (ay < by) ? 1 : -1;
int e = 2 * dy - dx;
if (steep)
{
for (int x = ax, y = ay; x <= bx; x++)
{
SetColor(y, x, color);
if (e >= 0)
{
y += stepY;
e -= 2 * dx;
}
e += 2 * dy;
}
}
else
{
for (int x = ax, y = ay; x <= bx; x++)
{
SetColor(x, y, color);
if (e >= 0)
{
y += stepY;
e -= 2 * dx;
}
e += 2 * dy;
}
}
}