如何画一条"直"线
如何在计算机上画一条直线
想必我们在小学二年级学函数的时候就已经背得滚瓜烂熟:直线公式
从”连续”到”离散”
数学上的直线由无数个没有大小的点组成,拥有无限的分辨率,是连续的。但显示器本质上是一个由像素(Pixel)组成的巨大网格,是离散的——每个像素有固定的位置,要么亮,要么不亮,不存在”半个像素”。

如上图所示,我们在计算机上只能”逼近”一条直线,画出一个个离理想直线最近的像素点。所幸现代显示器的分辨率足够高,肉眼几乎看不出锯齿。
光栅化(Rasterization):将矢量图形(点、线、面)转换为像素点阵的过程,是计算机图形学中最基础的操作之一。无论是游戏中的 3D 场景还是 GUI 界面上的一个按钮,最终都要经过光栅化才能显示在屏幕上。
直线算法
问题定义:给定直线两端点
下面介绍两种经典算法:DDA 和 Bresenham。其中 Bresenham 算法尤其重要——几乎所有规则图形的绘制都离不开它的思想。
DDA 算法(Digital Differential Analyzer)
DDA 算法的核心思想很朴素:从起点出发,每一步沿直线前进一个微小增量,四舍五入取最近的像素点。
推导
设直线斜率为
根据斜率大小,选择一个方向作为”主方向”,每次前进 1 个单位,另一方向前进
当
时( 方向变化更快):以 为主方向。每次 前进 1, 前进 : 当
时( 方向变化更快):以 为主方向。每次 前进 1, 前进 :
由于像素坐标必须是整数,每步计算出的
算法步骤
以
- 计算
,初始点 - 绘制
- 更新
- 重复步骤 2-3,直到
下面这张图直观展示了 DDA 的执行过程:

DDA 算法的特点
| 优点 | 缺点 |
|---|---|
| 思想直观,易于理解和实现 | 每步都需要浮点数加法,速度较慢 |
| 增量计算,无需每次重新求值 | round() 取整也是耗时操作 |
| 适用于任意斜率的直线 | 浮点累加会产生误差积累 |
| 不利于硬件实现——浮点运算单元远比整数运算单元复杂 |
Bresenham 算法
DDA 的致命弱点是依赖浮点数运算。1962 年,IBM 的 Jack Bresenham 提出了一种仅用整数运算的直线算法,至今仍是图形学中最优雅的算法之一。
直线的一般方程
将直线方程
两边乘
对于直线上的点,
- 直线上方的点:
- 直线下方的点:
平面被这条直线分成了三个区域:

的符号就像一个指南针,告诉我们当前点在直线的哪一侧。
核心思想:中点决策
以下推导假设
假设当前已画出像素点
- E(East):
——保持 不变 - NE(Northeast):
—— 加 1

如何选择?Bresenham 的巧妙之处在于:考察两个候选点之间的中点
- 若
在直线上方( ),说明理想直线从 下方穿过 → 更近 → 选 E - 若
在直线下方( ),说明理想直线从 上方穿过 → 更近 → 选 NE
将
递推公式
直接每一步都重新计算
当选 E 时:下一个中点为
当选 NE 时:下一个中点为
初始值
第一个中点取
至此,算法每步只需要比较
这样一来,整个算法中只涉及整数加减法和位左移(乘 2),没有任何浮点数运算!
算法步骤( )
- 输入端点
和 - 计算
, , , , - 绘制点
;若 ,则 ;否则 , - 若
,重复步骤 3-4
推广到任意斜率
上面只讨论了第一象限
| 象限 | 斜率范围 | 处理方式 |
|---|---|---|
| 第一象限 | 原算法,x 为主方向,E/NE 决策 | |
| 第一象限 | 交换 x、y 角色,y 为主方向 | |
| 第二象限 | x 递减,交换 x、y | |
| 第二象限 | x 递减,y 可能递减 | |
| … | … | 按符号对称处理 |
实现时可以根据
DDA vs Bresenham 对比
| DDA | Bresenham | |
|---|---|---|
| 运算类型 | 浮点数加法和舍入 | 仅整数加减法 |
| 每步计算量 | 1 次浮点加 + 1 次取整 | 1 次比较 + 1 次整数加 |
| 硬件友好度 | 低(需要 FPU) | 高(仅需 ALU) |
| 精度 | 浮点累加可能漂移 | 精确,无累积误差 |
| 直观性 | 非常直观 | 需要一点推导 |
| 适用范围 | 仅直线 | 思想可推广到圆、椭圆等 |
下一篇我们还是会围绕Bresenham算法展开,简单讲解一下讲解圆和椭圆的绘制。
这次的插图来自画师luoyu