Skip to content

第5章:2D 图形绘制算法

5.1 直线绘制算法

5.1.1 问题定义

在光栅显示器上,图像由离散的像素点组成。绘制直线的核心问题是:如何选择最佳的像素点来近似表示一条数学上连续的直线?

直线光栅化问题

数学直线(连续):          光栅化后(离散):

    │                           │
    │    ╱                      │    □
    │   ╱                       │   □
    │  ╱                        │  □
    │ ╱                         │ □ □
    │╱                          │□
────┼────                   ────┼────
    │                           │

问题:如何选择像素使直线看起来"最直"?

5.1.2 DDA 算法(Digital Differential Analyzer)

DDA 是最直观的直线绘制算法,基于直线的参数方程:

DDA 算法原理

直线方程:y = kx + b,其中 k = Δy/Δx

如果 |k| ≤ 1(偏水平):
- x 每次增加 1
- y 每次增加 k

如果 |k| > 1(偏垂直):
- y 每次增加 1
- x 每次增加 1/k


图示(k = 0.5 的情况):

步骤:x=0, y=0
      x=1, y=0.5 → 取整为 (1, 1) 或 (1, 0)
      x=2, y=1.0 → (2, 1)
      x=3, y=1.5 → (3, 2) 或 (3, 1)
      ...

    y
    3 │              □
    2 │         □  □
    1 │    □  □
    0 │ □
      └───────────────► x
        0  1  2  3  4

5.1.3 DDA 代码实现

javascript
/**
 * DDA 直线绘制算法
 * @param x1, y1 起点
 * @param x2, y2 终点
 * @param setPixel 绘制像素的回调函数
 */
function drawLineDDA(x1, y1, x2, y2, setPixel) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    
    // 计算步数(取 dx 和 dy 的较大者)
    const steps = Math.max(Math.abs(dx), Math.abs(dy));
    
    if (steps === 0) {
        setPixel(Math.round(x1), Math.round(y1));
        return;
    }
    
    // 计算每步的增量
    const xIncrement = dx / steps;
    const yIncrement = dy / steps;
    
    let x = x1;
    let y = y1;
    
    for (let i = 0; i <= steps; i++) {
        setPixel(Math.round(x), Math.round(y));
        x += xIncrement;
        y += yIncrement;
    }
}

// 使用示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

function setPixel(x, y) {
    ctx.fillRect(x, y, 1, 1);
}

drawLineDDA(10, 10, 100, 60, setPixel);

5.1.4 Bresenham 直线算法

Bresenham 算法是更高效的直线绘制算法,只使用整数运算:

Bresenham 算法原理

核心思想:
在每个 x 位置,判断下一个像素应该选择:
- 当前 y(保持不变)
- y + 1(上移)

判断依据是比较实际直线与两个候选像素的距离。


决策变量 d:
d = 2Δy - Δx(初始值)

如果 d < 0:
- 选择 (x+1, y)
- d = d + 2Δy

如果 d ≥ 0:
- 选择 (x+1, y+1)
- d = d + 2Δy - 2Δx


图示:

对于点 (x, y),下一个点是 (x+1, y) 还是 (x+1, y+1)?

        │      ╱ 实际直线
    y+1 │    ○╱     ← 候选点 B
        │    ╱│
        │   ╱ │ d1
        │  ╱──│
        │ ╱   │ d2
    y   │○────┴──    ← 候选点 A

        └─────────────
              x   x+1

如果 d1 < d2,选择 A;否则选择 B

5.1.5 Bresenham 代码实现

javascript
/**
 * Bresenham 直线算法
 * @param x0, y0 起点
 * @param x1, y1 终点
 * @param setPixel 绘制像素的回调函数
 */
function drawLineBresenham(x0, y0, x1, y1, setPixel) {
    // 计算差值
    let dx = Math.abs(x1 - x0);
    let dy = Math.abs(y1 - y0);
    
    // 确定步进方向
    const sx = x0 < x1 ? 1 : -1;
    const sy = y0 < y1 ? 1 : -1;
    
    // 判断斜率是否大于1
    const steep = dy > dx;
    
    if (steep) {
        // 如果斜率>1,交换 dx 和 dy
        [dx, dy] = [dy, dx];
    }
    
    // 初始决策变量
    let d = 2 * dy - dx;
    
    let x = x0;
    let y = y0;
    
    for (let i = 0; i <= dx; i++) {
        setPixel(x, y);
        
        if (d > 0) {
            if (steep) {
                x += sx;
            } else {
                y += sy;
            }
            d -= 2 * dx;
        }
        
        if (steep) {
            y += sy;
        } else {
            x += sx;
        }
        d += 2 * dy;
    }
}

// 更简洁的通用版本
function drawLine(x0, y0, x1, y1, setPixel) {
    const dx = Math.abs(x1 - x0);
    const dy = Math.abs(y1 - y0);
    const sx = x0 < x1 ? 1 : -1;
    const sy = y0 < y1 ? 1 : -1;
    
    let err = dx - dy;
    
    while (true) {
        setPixel(x0, y0);
        
        if (x0 === x1 && y0 === y1) break;
        
        const e2 = 2 * err;
        
        if (e2 > -dy) {
            err -= dy;
            x0 += sx;
        }
        if (e2 < dx) {
            err += dx;
            y0 += sy;
        }
    }
}

5.2 圆绘制算法

5.2.1 圆的特性

圆的对称性

圆心在原点的圆具有 8 重对称性:

如果 (x, y) 在圆上,则以下 7 个点也在圆上:
(-x, y), (x, -y), (-x, -y)
(y, x), (-y, x), (y, -x), (-y, -x)


        y

    ────┼────
   ╱    │    ╲
  ╱     │     ╲
 │   2  │  1   │
 │ (-x,y)│(x,y)│
─┼──────┼──────┼─ x
 │ 3    │    8 │
 │(-x,-y)│(x,-y)│
  ╲     │     ╱
   ╲    │    ╱
    ────┼────


利用对称性,只需要计算 1/8 的圆弧!

5.2.2 中点圆算法

中点圆算法(Midpoint Circle Algorithm)

基于圆的隐式方程:F(x, y) = x² + y² - r² = 0

F(x, y) < 0:点在圆内
F(x, y) = 0:点在圆上
F(x, y) > 0:点在圆外


决策过程:

从 (0, r) 开始,向右下方遍历(在第一象限的第一段弧)

对于当前点 (x, y),下一个点是:
- E: (x+1, y)
- SE: (x+1, y-1)

判断中点 M = (x+1, y-0.5) 在圆内还是圆外


        (x, y)

           │╲
           │ ╲
     M ────●  ╲
           │   ○ (x+1, y-1)

           ○ (x+1, y)

如果 F(M) < 0,M 在圆内,选择 E
如果 F(M) ≥ 0,M 在圆外或圆上,选择 SE

5.2.3 中点圆算法实现

javascript
/**
 * 中点圆算法
 * @param cx, cy 圆心
 * @param r 半径
 * @param setPixel 绘制像素的回调
 */
function drawCircleMidpoint(cx, cy, r, setPixel) {
    // 利用 8 重对称性绘制 8 个点
    function drawCirclePoints(x, y) {
        setPixel(cx + x, cy + y);
        setPixel(cx - x, cy + y);
        setPixel(cx + x, cy - y);
        setPixel(cx - x, cy - y);
        setPixel(cx + y, cy + x);
        setPixel(cx - y, cy + x);
        setPixel(cx + y, cy - x);
        setPixel(cx - y, cy - x);
    }
    
    let x = 0;
    let y = r;
    
    // 初始决策变量:d = F(1, r-0.5) = 1 + (r-0.5)² - r² = 1.25 - r
    // 为避免浮点运算,使用 4d 代替 d
    let d = 1 - r;
    
    drawCirclePoints(x, y);
    
    while (x < y) {
        if (d < 0) {
            // 选择 E 点
            d += 2 * x + 3;
        } else {
            // 选择 SE 点
            d += 2 * (x - y) + 5;
            y--;
        }
        x++;
        drawCirclePoints(x, y);
    }
}

// Bresenham 圆算法(另一种实现)
function drawCircleBresenham(cx, cy, r, setPixel) {
    let x = 0;
    let y = r;
    let d = 3 - 2 * r;
    
    function drawCirclePoints(x, y) {
        setPixel(cx + x, cy + y);
        setPixel(cx - x, cy + y);
        setPixel(cx + x, cy - y);
        setPixel(cx - x, cy - y);
        setPixel(cx + y, cy + x);
        setPixel(cx - y, cy + x);
        setPixel(cx + y, cy - x);
        setPixel(cx - y, cy - x);
    }
    
    drawCirclePoints(x, y);
    
    while (y >= x) {
        x++;
        
        if (d > 0) {
            y--;
            d = d + 4 * (x - y) + 10;
        } else {
            d = d + 4 * x + 6;
        }
        
        drawCirclePoints(x, y);
    }
}

5.3 椭圆绘制

5.3.1 椭圆的特性

椭圆方程

标准椭圆方程:x²/a² + y²/b² = 1

其中 a 是水平半轴,b 是垂直半轴


椭圆的对称性:

椭圆有 4 重对称性(相比圆的 8 重)

如果 (x, y) 在椭圆上,则:
(-x, y), (x, -y), (-x, -y) 也在椭圆上


椭圆分区:

由于椭圆不是圆,需要将其分为两个区域:

区域 1:斜率 |dy/dx| < 1(靠近水平轴)
区域 2:斜率 |dy/dx| > 1(靠近垂直轴)

分界点:斜率 = -1 的位置

        y
        │     区域2
        │  ╱────────╲
        │ ╱          ╲
        │╱   区域1    ╲
        ○──────────────○─► x
        │╲            ╱
        │ ╲          ╱
        │  ╲────────╱

5.3.2 中点椭圆算法

javascript
/**
 * 中点椭圆算法
 * @param cx, cy 中心点
 * @param a 水平半轴
 * @param b 垂直半轴
 * @param setPixel 绘制像素的回调
 */
function drawEllipseMidpoint(cx, cy, a, b, setPixel) {
    // 绘制 4 个对称点
    function drawEllipsePoints(x, y) {
        setPixel(cx + x, cy + y);
        setPixel(cx - x, cy + y);
        setPixel(cx + x, cy - y);
        setPixel(cx - x, cy - y);
    }
    
    const a2 = a * a;
    const b2 = b * b;
    const twoA2 = 2 * a2;
    const twoB2 = 2 * b2;
    
    let x = 0;
    let y = b;
    
    // 区域 1
    let px = 0;
    let py = twoA2 * y;
    
    // 区域 1 的初始决策变量
    let d1 = b2 - (a2 * b) + (0.25 * a2);
    
    drawEllipsePoints(x, y);
    
    // 区域 1:当 b²x < a²y(斜率 > -1)
    while (px < py) {
        x++;
        px += twoB2;
        
        if (d1 < 0) {
            d1 += b2 + px;
        } else {
            y--;
            py -= twoA2;
            d1 += b2 + px - py;
        }
        
        drawEllipsePoints(x, y);
    }
    
    // 区域 2 的初始决策变量
    let d2 = b2 * (x + 0.5) * (x + 0.5) + 
             a2 * (y - 1) * (y - 1) - 
             a2 * b2;
    
    // 区域 2:当 b²x >= a²y(斜率 <= -1)
    while (y > 0) {
        y--;
        py -= twoA2;
        
        if (d2 > 0) {
            d2 += a2 - py;
        } else {
            x++;
            px += twoB2;
            d2 += a2 - py + px;
        }
        
        drawEllipsePoints(x, y);
    }
}

5.4 多边形填充

5.4.1 填充算法概述

多边形填充的主要算法

1. 种子填充(Flood Fill)
   - 从内部一点开始,向外扩散
   - 简单但效率低

2. 扫描线填充(Scanline Fill)
   - 按行扫描,找交点,填充区间
   - 效率高,适合凸/凹多边形

3. 边界填充(Boundary Fill)
   - 从内部开始,遇到边界停止
   - 适合简单形状

5.4.2 扫描线填充算法

扫描线填充原理

对于每条水平扫描线:
1. 找到与多边形边的所有交点
2. 将交点按 x 坐标排序
3. 两两配对,填充之间的像素


示例:

多边形:
       ╱╲
      ╱  ╲
     ╱    ╲
    ╱      ╲
   ╱────────╲
  ╱          ╲
 ╱────────────╲


扫描线 y=3:

   ╱╲               │
  ╱  ╲           ───┼───  y=3
 ╱────╲          ●  │  ●
╱      ╲            │

      交点 x1    交点 x2

填充 [x1, x2] 之间的像素

5.4.3 扫描线填充实现

javascript
/**
 * 扫描线多边形填充
 * @param vertices 多边形顶点数组 [{x, y}, ...]
 * @param setPixel 绘制像素的回调
 */
function fillPolygonScanline(vertices, setPixel) {
    if (vertices.length < 3) return;
    
    // 找到 y 的范围
    let minY = Infinity;
    let maxY = -Infinity;
    
    for (const v of vertices) {
        minY = Math.min(minY, v.y);
        maxY = Math.max(maxY, v.y);
    }
    
    minY = Math.ceil(minY);
    maxY = Math.floor(maxY);
    
    // 构建边表
    const edges = [];
    const n = vertices.length;
    
    for (let i = 0; i < n; i++) {
        const v1 = vertices[i];
        const v2 = vertices[(i + 1) % n];
        
        // 跳过水平边
        if (v1.y === v2.y) continue;
        
        // 确保 v1.y < v2.y
        const [p1, p2] = v1.y < v2.y ? [v1, v2] : [v2, v1];
        
        edges.push({
            yMin: p1.y,
            yMax: p2.y,
            x: p1.x,
            slope: (p2.x - p1.x) / (p2.y - p1.y)
        });
    }
    
    // 按 yMin 排序边
    edges.sort((a, b) => a.yMin - b.yMin);
    
    // 活动边列表
    let activeEdges = [];
    let edgeIndex = 0;
    
    // 逐行扫描
    for (let y = minY; y <= maxY; y++) {
        // 添加新的活动边
        while (edgeIndex < edges.length && edges[edgeIndex].yMin <= y) {
            activeEdges.push({ ...edges[edgeIndex] });
            edgeIndex++;
        }
        
        // 移除过期的边
        activeEdges = activeEdges.filter(e => e.yMax > y);
        
        // 按 x 排序活动边
        activeEdges.sort((a, b) => a.x - b.x);
        
        // 配对填充
        for (let i = 0; i < activeEdges.length - 1; i += 2) {
            const x1 = Math.ceil(activeEdges[i].x);
            const x2 = Math.floor(activeEdges[i + 1].x);
            
            for (let x = x1; x <= x2; x++) {
                setPixel(x, y);
            }
        }
        
        // 更新边的 x 值
        for (const edge of activeEdges) {
            edge.x += edge.slope;
        }
    }
}

// 使用示例
const triangle = [
    { x: 100, y: 50 },
    { x: 50, y: 150 },
    { x: 150, y: 150 }
];

fillPolygonScanline(triangle, setPixel);

5.4.4 种子填充算法

javascript
/**
 * 递归种子填充(Flood Fill)
 * 注意:大区域可能导致栈溢出
 */
function floodFillRecursive(x, y, targetColor, fillColor, getPixel, setPixel) {
    if (getPixel(x, y) !== targetColor) return;
    
    setPixel(x, y, fillColor);
    
    floodFillRecursive(x + 1, y, targetColor, fillColor, getPixel, setPixel);
    floodFillRecursive(x - 1, y, targetColor, fillColor, getPixel, setPixel);
    floodFillRecursive(x, y + 1, targetColor, fillColor, getPixel, setPixel);
    floodFillRecursive(x, y - 1, targetColor, fillColor, getPixel, setPixel);
}

/**
 * 非递归种子填充(使用队列)
 */
function floodFillIterative(startX, startY, targetColor, fillColor, getPixel, setPixel, width, height) {
    if (getPixel(startX, startY) !== targetColor) return;
    
    const queue = [[startX, startY]];
    const visited = new Set();
    
    while (queue.length > 0) {
        const [x, y] = queue.shift();
        const key = `${x},${y}`;
        
        if (visited.has(key)) continue;
        if (x < 0 || x >= width || y < 0 || y >= height) continue;
        if (getPixel(x, y) !== targetColor) continue;
        
        visited.add(key);
        setPixel(x, y, fillColor);
        
        queue.push([x + 1, y]);
        queue.push([x - 1, y]);
        queue.push([x, y + 1]);
        queue.push([x, y - 1]);
    }
}

/**
 * 扫描线种子填充(更高效)
 */
function floodFillScanline(startX, startY, targetColor, fillColor, getPixel, setPixel, width, height) {
    if (getPixel(startX, startY) !== targetColor) return;
    
    const stack = [[startX, startY]];
    
    while (stack.length > 0) {
        let [x, y] = stack.pop();
        
        // 向左找到边界
        while (x > 0 && getPixel(x - 1, y) === targetColor) {
            x--;
        }
        
        let spanAbove = false;
        let spanBelow = false;
        
        // 向右扫描填充
        while (x < width && getPixel(x, y) === targetColor) {
            setPixel(x, y, fillColor);
            
            // 检查上方
            if (!spanAbove && y > 0 && getPixel(x, y - 1) === targetColor) {
                stack.push([x, y - 1]);
                spanAbove = true;
            } else if (spanAbove && y > 0 && getPixel(x, y - 1) !== targetColor) {
                spanAbove = false;
            }
            
            // 检查下方
            if (!spanBelow && y < height - 1 && getPixel(x, y + 1) === targetColor) {
                stack.push([x, y + 1]);
                spanBelow = true;
            } else if (spanBelow && y < height - 1 && getPixel(x, y + 1) !== targetColor) {
                spanBelow = false;
            }
            
            x++;
        }
    }
}

5.5 抗锯齿

5.5.1 锯齿问题

锯齿(Aliasing)

由于光栅化将连续图形离散化,产生了阶梯状的"锯齿"。

理想直线:                光栅化后:
    ╱                        □
   ╱                       □
  ╱                      □ □
 ╱                     □ □
╱                    □ □

锯齿在斜线、曲线边缘特别明显。


抗锯齿(Anti-aliasing):
通过颜色混合,使边缘看起来更平滑。

5.5.2 抗锯齿直线(Wu's Algorithm)

吴小林抗锯齿算法

原理:根据像素与理想直线的距离,设置不同的透明度。


        │  ■ 100%(直线穿过)
        │  ▪ 60%(部分覆盖)
        │  · 20%(轻微覆盖)

────────┼────────



对于非整数坐标 (x, y.3):
- (x, floor(y)) 的强度 = 1 - 0.3 = 0.7
- (x, ceil(y))  的强度 = 0.3

          y

    y+1   │  ▪ 30%
          │  ╱
          │ ╱
    y     │■ 70%
          └───────► x

5.5.3 抗锯齿直线实现

javascript
/**
 * 吴小林抗锯齿直线算法
 * @param x0, y0 起点
 * @param x1, y1 终点
 * @param setPixelAlpha 设置像素及透明度的回调 (x, y, alpha)
 */
function drawLineAntialiased(x0, y0, x1, y1, setPixelAlpha) {
    // 判断是否更陡峭
    const steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
    
    if (steep) {
        [x0, y0] = [y0, x0];
        [x1, y1] = [y1, x1];
    }
    
    if (x0 > x1) {
        [x0, x1] = [x1, x0];
        [y0, y1] = [y1, y0];
    }
    
    const dx = x1 - x0;
    const dy = y1 - y0;
    const gradient = dx === 0 ? 1 : dy / dx;
    
    // 绘制像素(考虑陡峭情况)
    function plot(x, y, alpha) {
        if (steep) {
            setPixelAlpha(Math.floor(y), Math.floor(x), alpha);
        } else {
            setPixelAlpha(Math.floor(x), Math.floor(y), alpha);
        }
    }
    
    // 辅助函数
    function fpart(x) {
        return x - Math.floor(x);
    }
    
    function rfpart(x) {
        return 1 - fpart(x);
    }
    
    // 处理起点
    let xend = Math.round(x0);
    let yend = y0 + gradient * (xend - x0);
    let xgap = rfpart(x0 + 0.5);
    const xpxl1 = xend;
    const ypxl1 = Math.floor(yend);
    
    plot(xpxl1, ypxl1, rfpart(yend) * xgap);
    plot(xpxl1, ypxl1 + 1, fpart(yend) * xgap);
    
    let intery = yend + gradient;
    
    // 处理终点
    xend = Math.round(x1);
    yend = y1 + gradient * (xend - x1);
    xgap = fpart(x1 + 0.5);
    const xpxl2 = xend;
    const ypxl2 = Math.floor(yend);
    
    plot(xpxl2, ypxl2, rfpart(yend) * xgap);
    plot(xpxl2, ypxl2 + 1, fpart(yend) * xgap);
    
    // 主循环
    for (let x = xpxl1 + 1; x < xpxl2; x++) {
        plot(x, Math.floor(intery), rfpart(intery));
        plot(x, Math.floor(intery) + 1, fpart(intery));
        intery += gradient;
    }
}

// 使用示例
function setPixelWithAlpha(x, y, alpha) {
    ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
    ctx.fillRect(x, y, 1, 1);
}

drawLineAntialiased(10, 10, 100, 60, setPixelWithAlpha);

5.6 裁剪算法

5.6.1 裁剪的概念

裁剪(Clipping)

裁剪是去除视窗外部图元的过程,只显示视窗内的部分。

          视窗
     ┌─────────────┐
     │             │
     │    ╱────╲   │
─────┼───╱──────╲──┼────  ← 部分可见
     │  ╱        ╲ │
     │ ╱          ╲│
     └─────────────┘

       需要裁剪


裁剪的目的:
1. 提高渲染效率(不渲染看不见的部分)
2. 保证正确显示(防止绘制到视窗外)

5.6.2 Cohen-Sutherland 直线裁剪

Cohen-Sutherland 算法

核心思想:使用区域码(Outcode)快速判断直线与视窗的关系。


区域码定义:

     1001 │ 1000 │ 1010
    ──────┼──────┼──────
     0001 │ 0000 │ 0010     ← 中间是视窗
    ──────┼──────┼──────
     0101 │ 0100 │ 0110

4 位分别表示:
- bit 0:左边 (x < xmin)
- bit 1:右边 (x > xmax)
- bit 2:下边 (y < ymin)
- bit 3:上边 (y > ymax)


判断规则:

1. 两端点的 outcode 都是 0000:完全在视窗内,接受
2. 两端点的 outcode 按位与 ≠ 0:完全在视窗外,拒绝
3. 其他情况:部分可见,需要裁剪

5.6.3 Cohen-Sutherland 实现

javascript
// 区域码
const INSIDE = 0; // 0000
const LEFT = 1;   // 0001
const RIGHT = 2;  // 0010
const BOTTOM = 4; // 0100
const TOP = 8;    // 1000

/**
 * 计算点的区域码
 */
function computeOutCode(x, y, xmin, ymin, xmax, ymax) {
    let code = INSIDE;
    
    if (x < xmin) code |= LEFT;
    else if (x > xmax) code |= RIGHT;
    
    if (y < ymin) code |= BOTTOM;
    else if (y > ymax) code |= TOP;
    
    return code;
}

/**
 * Cohen-Sutherland 直线裁剪
 * @returns 裁剪后的线段 {x0, y0, x1, y1} 或 null(完全在外)
 */
function clipLineCohenSutherland(x0, y0, x1, y1, xmin, ymin, xmax, ymax) {
    let outcode0 = computeOutCode(x0, y0, xmin, ymin, xmax, ymax);
    let outcode1 = computeOutCode(x1, y1, xmin, ymin, xmax, ymax);
    
    while (true) {
        if ((outcode0 | outcode1) === 0) {
            // 完全在内部
            return { x0, y0, x1, y1 };
        }
        
        if ((outcode0 & outcode1) !== 0) {
            // 完全在外部
            return null;
        }
        
        // 部分可见,需要裁剪
        const outcodeOut = outcode0 !== 0 ? outcode0 : outcode1;
        let x, y;
        
        // 找到与边界的交点
        if (outcodeOut & TOP) {
            x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
            y = ymax;
        } else if (outcodeOut & BOTTOM) {
            x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
            y = ymin;
        } else if (outcodeOut & RIGHT) {
            y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
            x = xmax;
        } else if (outcodeOut & LEFT) {
            y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
            x = xmin;
        }
        
        // 更新端点
        if (outcodeOut === outcode0) {
            x0 = x;
            y0 = y;
            outcode0 = computeOutCode(x0, y0, xmin, ymin, xmax, ymax);
        } else {
            x1 = x;
            y1 = y;
            outcode1 = computeOutCode(x1, y1, xmin, ymin, xmax, ymax);
        }
    }
}

// 使用示例
const clipped = clipLineCohenSutherland(
    -10, 20, 150, 80,  // 线段
    0, 0, 100, 100     // 视窗
);

if (clipped) {
    drawLine(clipped.x0, clipped.y0, clipped.x1, clipped.y1, setPixel);
}

5.6.4 Sutherland-Hodgman 多边形裁剪

javascript
/**
 * Sutherland-Hodgman 多边形裁剪
 * @param polygon 多边形顶点数组
 * @param clipRect 裁剪矩形 {xmin, ymin, xmax, ymax}
 * @returns 裁剪后的多边形顶点
 */
function clipPolygonSutherlandHodgman(polygon, clipRect) {
    const { xmin, ymin, xmax, ymax } = clipRect;
    
    // 定义四条边
    const edges = [
        { // 左边
            inside: (p) => p.x >= xmin,
            intersect: (p1, p2) => ({
                x: xmin,
                y: p1.y + (p2.y - p1.y) * (xmin - p1.x) / (p2.x - p1.x)
            })
        },
        { // 右边
            inside: (p) => p.x <= xmax,
            intersect: (p1, p2) => ({
                x: xmax,
                y: p1.y + (p2.y - p1.y) * (xmax - p1.x) / (p2.x - p1.x)
            })
        },
        { // 下边
            inside: (p) => p.y >= ymin,
            intersect: (p1, p2) => ({
                x: p1.x + (p2.x - p1.x) * (ymin - p1.y) / (p2.y - p1.y),
                y: ymin
            })
        },
        { // 上边
            inside: (p) => p.y <= ymax,
            intersect: (p1, p2) => ({
                x: p1.x + (p2.x - p1.x) * (ymax - p1.y) / (p2.y - p1.y),
                y: ymax
            })
        }
    ];
    
    let output = polygon;
    
    // 依次对四条边裁剪
    for (const edge of edges) {
        if (output.length === 0) break;
        
        const input = output;
        output = [];
        
        for (let i = 0; i < input.length; i++) {
            const current = input[i];
            const next = input[(i + 1) % input.length];
            
            const currentInside = edge.inside(current);
            const nextInside = edge.inside(next);
            
            if (currentInside) {
                if (nextInside) {
                    // 两点都在内部:添加下一点
                    output.push(next);
                } else {
                    // 出去了:添加交点
                    output.push(edge.intersect(current, next));
                }
            } else {
                if (nextInside) {
                    // 进来了:添加交点和下一点
                    output.push(edge.intersect(current, next));
                    output.push(next);
                }
                // 两点都在外部:不添加任何点
            }
        }
    }
    
    return output;
}

5.7 本章小结

核心算法

算法用途特点
DDA直线绘制简单,浮点运算
Bresenham 直线直线绘制高效,纯整数
中点圆算法圆绘制利用对称性
中点椭圆算法椭圆绘制分区处理
扫描线填充多边形填充高效,通用
种子填充区域填充简单,递归/迭代
Wu's 抗锯齿抗锯齿直线平滑边缘
Cohen-Sutherland直线裁剪区域码快速判断

算法选择指南

选择直线算法:
- 精度要求不高 → DDA
- 追求效率 → Bresenham
- 需要平滑 → Wu's 抗锯齿

选择填充算法:
- 简单形状、交互式 → 种子填充
- 复杂多边形 → 扫描线填充

选择裁剪算法:
- 直线裁剪 → Cohen-Sutherland
- 多边形裁剪 → Sutherland-Hodgman

下一章预告:在第6章中,我们将学习曲线与曲面的数学表示,包括贝塞尔曲线、B-样条和 NURBS。


文档版本:v1.0
字数统计:约 12,000 字

如有转载或 CV 的请标注本站原文地址