跳转至

直线段光栅化

别名: 直线扫描转换算法.

该部分算法用于将直线段的顶点表示转换为点阵表示, 该过程被称为直线段的"光栅化".

数值微分法(Digital Differential Analyzer, DDA)

理论

思想: 增量.
因为光栅的光学器件等宽等间距, 所以在绘制线段的过程中, x 的增量恒为 1. 只要通过直线的斜截式计算出斜率 k, 即可只通过两次浮点数加法(包括四舍五入的)得出下一个点的坐标.
公式 1: y = k * x + b, k = Δy / Δx (直线的斜截式方程)

实现

void drawLine(Point2 start, Point2 end)
{
  const float dx = end.x - start.x;
  const float dy = end.y - start.y;
  const float k  = dy / dx;

  float y = start.y;
  for(int x = (int)std::round(start.x); x <= end.x; x++) // 对 x 四舍五入取整.  
  {
    drawPoint(x, (int)std::round(y));
    y += k;
  }
}

当线段平行于 y 轴时, 变量 dx 为零, 而零作为除数没有意义. 因此该实现只适用于线段不与 y 轴平行的情况.
四舍五入的不完整简单实现方法有加上 0.5 然后再取整, 此处使用 std::round 以确保代码的正确性.

完善

void drawLine(Point2 start, Point2 end)
{
  const float dx = end.x - start.x;
  const float dy = end.y - start.y;
  const float dm = std::max(dx, dy);
  const float kx = max / dx;
  const float ky = max / dy;

  float x = start.x, y = start.y;
  for(int i = 0; i < dm; i++)
  {
    drawPoint((int)std::round(x), (int)std::round(y));
    x += kx, y += ky;
  }
}

上面的实现效率较低, 但适用于各种情况.

void drawLine(Point2 start, Point2 end)
{
  const float dx = end.x - start.x;
  const float dy = end.y - start.y;

  if(std::abs(dx) < std::abs(dy)) // k > 1
  {
    const float k = dx / dy;
    float x = start.x;
    for(int y = (int)std::round(start.y); y <= end.y; y++)
    {
      drawPoint((int)std::round(x), y);
      x += k;
    }
  }
  else if(std::abs(dx) > std::abs(dy)) // k < 1
  {
    const float k = dy / dx;
    float y = start.y;
    for(int x = (int)std::round(start.x); x <= end.x; x++)
    {
      drawPoint(x, (int)std::round(y));
      y += k;
    }
  }
  else // k == 1
  {
    int y = (int)std::round(start.y);
    for(int x = (int)std::round(start.x); x <= end.x; x++, y++)
      drawPoint(x, y);
  }
}

上面的实现对斜率进行分类讨论, 针对不同情况使用单独的略有不同的实现. 可以通过交换 start 与 end 的 x 和 y 坐标精简代码, 但可能会影响可读性此处没有采用.

中点分割法

理论

DDA 绘制一个点平均使用两至三个浮点数加法. 浮点数加法慢与整数加法, 若能使用整数加法替代浮点数加法可以得到更高的性能.
该算法利用直线的一般式, 完全使用整数加法.
设 F(x, y) = Ax + By + C, 根据直线的一般式可得:

  • 若点位于直线上方, 则 F(x, y) > 0
  • 若点位于直线上, 则 F(x, y) = 0
  • 若点位于直线下方, 则 F(x, y) < 0

Figure-0

每次向最大位移方向上累加一个单元, 通过中点误差项判断是否往另一个方向累加一个单元. 若 0 <= |k| <= 1, 即|Δx| > |Δy|, 每次循环时 x 坐标累加 1, y 坐标是否累加 1 取决于线段与 x = x(变量) 交点 Q 的位置, 离点 Pd(x + 1, y) 更近还是点 Pu(x + 1, y + 1). 这将通过把这两点的中点 M 带入 F(x, y) 后判断其正负性. 若 F(x, y) < 0, 则表示中点位于交点 Q 的下方, 即交点 Q 位于中点 M 上方, 更接近于点 (x, y + 1).
公式 2: Ax + By + C = 0, A = -(Δy), B = Δx, C = -B(Δx) (直线的一般式方程)

实现

void drawLine(Point2 start, Point2 end)
{
  const auto dx = end.x - start.x;
  const auto dy = end.y - start.y;

  const auto a = -dy, b = dx, c = -b * dx;
  auto f = [a, b, c](int x, int y){ return a * x + b * y + c; };

  int y = (int)std::round(start.y);
  for(int x = (int)std::round(start.x); x <= end.x; x++)
  {
    drawPoint(x, y);
    auto d = f(x + 1, y + 0.5f);
    y += d < 0; // if(d < 0) y++;
  }
}

上面实现效率低下, 主要有以下几点可以改进的部分:

  • 主要的计算工作在 lambda 函数 f 中, 用以计算 d. 若推导出 d 值的递推公式, 则可以删减这部分计算.
  • 若上一次计算的 d >= 0, 上一个中点为 0(x, y + 0.5), 下一个中点为 M1(x + 1, y + 0.5) 带入 F 可以看出 A 的系数增加了 1, B 的系数加了 1. 所以 F(M1) - F(M0) = A + B, 即 d1 - d0 = A + B, d1 = d0 + A + B.
  • 若上一次计算的 d < 0, 上一个中点为 M0(x, y + 0.5), 下一个中点为 M1(x + 1, y + 0.5), 带入 F 可以看出 A 的系数增加了 1, B 的系数不变. 所以 F(M1) - F(M0) = A, 即 d1 - d0 = A, d1 = d0 + A.

由此可知可以通过增量计算出下一个 d 的值, 而不需要每次都将中点带入函数 F 计算. d 的初始值 d0 可以通过以下方式计算.
d0 = F(x0 + 1, y0 + 0.5) = F(x0, y0) + A + 0.5B. 因为 (x0, y0) 为直线起始点, 位于直线上, 所以 F(x0, y0) = 0. 所以 d0 = A + 0.5B. - 求 d0 以及每次将中点 M 的值代入函数 F 中时, 需要进行一次浮点数加法. 由于 d 只被用于判断其正负性, 所有可以通过乘以2来消除浮点数加法(0.5f * 2 = 1).

综上所述, 可以得到以下面实现.

void drawLine(Point2 start, Point2 end)
{
  const auto dx = end.x - start.x;
  const auto dy = end.y - start.y;
  const auto a  = -dy, b = dx;
  int        dd = a + a + b + b; // 2 * d0 = 2 * (A + B)

  int y = (int)std::round(start.y)
  for(int x = (int)std::round(start.x); x <= end.x; x++)
  {
    drawPoint(x, y);
    if(dd < 0)
    {
      y++;
      dd += a + a + b + b; // 2 * (A + B)
    }
    else
      dd += a + a; // 2 * A
  }
}

不过上面的实现明显并不适用于斜率大于 1 的线段, 因为每次循环 x 累加 1, 却只会调用一次 drawPoint. 如果强行使用这类实现绘制斜率大于 1 的线段, 将会发现绘制出的线段在 y 轴方向上不连续. 若要实现可绘制各种线段的, 可以参考 DDA 部分的解决方案.

Bresenham 算法

背景: 该算法由 E. Jack Bresenham 提出, 该算法是其最著名的发明.

理论

Figure-1
由 Figure-1 可以看出 d 的计算方法为:

d += k;
d -= d >= 1; // if(d >= 1) d--;

评论