一文理解Bresenham‘s line algorithm

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(x2x1),y=y1+t(y2y1),t[0,1].t=0At=1Bt[0,1]AB线A(x1,y1,z1)B(x2,y2,z2)x=x1+t(x2x1),y=y1+t(y2y1),z=z1+t(z2z1),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,y1Z)线Δx=x1x0>0m=ΔxΔy(0m1)
定义理想直线方程
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(xx0)+y0x=xk+1yyideal=m(xk+1x0)+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 )。 ABC(xk+1,yk+0.5)ek=yideal(yk+0.5)CekAB2Δxek=2Δx(yidealyk0.5)ekABek<0A(y)ek0B(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)yk0.5)yideal(k+1)=yideal(k)+mek+1=ek+2ΔyB(y)ek+1=2Δx(yideal(k+1)(yk+1)0.5)ek+1=ek+2Δy2Δ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Δy2Δ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=y0e1=2Δx(yideal(1)y00.5),yideal(1)=yideal(0)+m,yideal(0)=y0e1=2ΔyΔx

扩展到所有八分圆

上 述 推 导 假 设 0 ≤ m ≤ 1 且 Δ x > 0 。 对 于 其 他 情 况 : 上述推导假设 0 ≤ m ≤ 1 且 \Delta x > 0 。对于其他情况: 0m1Δx>0

斜率 m > 1 : 交 换 x 和 y 的 角 色 , 沿 y 方 向 步 进 。 负斜率 : 调 整 步 进 方 向 为 − 1 。 反向绘制 ( Δ x < 0 ) : 交 换 起 点 和 终 点 。 \textbf{斜率} m > 1 :交换 x 和 y 的角色,沿 y 方向步进。 \\ \textbf{负斜率}:调整步进方向为 -1 。 \\ \textbf{反向绘制}( \Delta x < 0 ):交换起点和终点。 斜率m>1xy沿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;
        }
    }
}

Reference

Bresenham’s line Algorithm wikipedia

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值