【计算机图形学】Bresenham直线绘制算法

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) 后,下一个像素点只有两种选择:

  1. 水平向右 :(x+1,y)(x+1, y)(x+1,y)
  2. 右上对角 :(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 为例)

  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
  1. 初始化决策参数

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


  1. 循环迭代 (从 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);
        }
    }
}
相关推荐
小鸡吃米…1 小时前
TensorFlow 实现异或(XOR)运算
人工智能·python·tensorflow·neo4j
小王不爱笑1321 小时前
检索增强生成 RAG
人工智能·机器学习
OpenCSG1 小时前
什么是模型管理平台?从大模型治理走向企业级OPC平台
大数据·人工智能·opencsg
bitbrowser2 小时前
AI搜索可见性:如何让产品在ChatGPT和DeepSeek里被优先展示?
人工智能·chatgpt
郝学胜-神的一滴2 小时前
FastAPI:Python 高性能 Web 框架的优雅之选
开发语言·前端·数据结构·python·算法·fastapi
sdyeswlw2 小时前
案例直击|一二三物联网 2025 三大经典项目,解锁物联赋能新场景
大数据·网络·人工智能
软件工程小施同学2 小时前
区块链可投会议CCF B--ICWS 2026 截止3.8
人工智能
万能菜道人2 小时前
学习Lora训练的个人记录
人工智能·学习
x-cmd2 小时前
[x-cmd] Firefox 148 发布 AI 开关,支持一键禁用 AI 功能
人工智能·ai·firefox·agent·x-cmd