惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
P
Proofpoint News Feed
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
C
Check Point Blog
Microsoft Azure Blog
Microsoft Azure Blog
aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
美团技术团队
D
Docker
博客园 - Franky
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

姓王者的博客

Linux用户Secure Boot自主维护指南 | 姓王者的博客 MAD Bugs 已经开始——关于信息安全的军备竞赛 | 姓王者的博客 解决钉钉Dingtalk无法在Linux新版内核上启动问题-修复可执行栈错误 | 姓王者的博客 突发:GitHub 正遭受大规模 Issue 赌博广告轰炸 | 姓王者的博客 Ubuntu26.04-beta体验:坚毅浣熊! | 姓王者的博客 fakeclaw装作龙虾发贴吧 | 姓王者的博客 找回12年前的QQ记忆 | 姓王者的博客 在Linux上玩Flash网页游戏-洛克王国 | 姓王者的博客 Copilot将使用交互数据来训练 | 姓王者的博客 重要通知-请更新我的GPG公钥 | 姓王者的博客 为了自由Android | 姓王者的博客 GPL"2,3"事 | 姓王者的博客 短文-对VitePlus的一点🤏小贡献 | 姓王者的博客 Bing收录没了?亲测有效的快速恢复指南 | 姓王者的博客 解决桌面设备二维码快速识别的工具-ClipQR | 姓王者的博客 解决 Nautilus 自定义终端插件安装依赖问题 | 姓王者的博客 OpenClaw 该熄火了 | 姓王者的博客 Vite8 - 统一的基建开始 | 姓王者的博客 Astro 6 推出啦 | 姓王者的博客 ubuntu的openvpn异常暂停推送更新 | 姓王者的博客 Ubuntu 24.04 安装 Win10 虚拟机 | 姓王者的博客 ESA-后记:热爱阿里云 | 姓王者的博客 Moonbit 0.8.0 重大发布,我也要改一下我的包 | 姓王者的博客 ESA Pages 边缘开发大赛获奖 | 姓王者的博客 Astro: 优化katex,mermaid和灯箱使用 | 姓王者的博客 从edgeone迁移到esa | 姓王者的博客 出租人类:AI时代的荒诞与真实 | 姓王者的博客 Astro 5.17构建性能优化实践:从18s到13s | 姓王者的博客 Moonbit License Checker 开发使用 | 姓王者的博客 Stalux Astro博客主题自荐 | 姓王者的博客
计算机图形学-基本图形生成算法 | 姓王者的博客
作者:xingwangzhe · 2025-09-20 · via 姓王者的博客

计算机图形学-基本图形生成算法

🕒 阅读时间:9 分钟 📝 字数:3294 👀 阅读量: Loading...

第四章 基本图形生成算法

4.1 引言

问题提出 在光栅设备(如显示器、打印机)上如何生成基本二维图形(点、直线、圆、椭圆、多边形、字符等)?

图形生成(扫描转换) 将连续的几何描述转换为离散像素集合,即在数字设备上确定哪些像素最能逼近目标图形。

关键目标

  • 保持图形逼真度
  • 端点和轮廓准确
  • 颜色与灰度均匀
  • 算法执行效率高
  • 支持绘制属性(颜色、线型、线宽等)

4.2 直线的扫描转换

4.2.1 数值微分法(DDA 算法)

算法原理

  • 给定起点 (x0,y0)(x_0, y_0) 和终点 (x1,y1)(x_1, y_1),计算差分 dx=x1−x0dx = x_1 - x_0dy=y1−y0dy = y_1 - y_0
  • 确定步数 N=max⁡(∣dx∣,∣dy∣)N = \max(|dx|, |dy|)
  • 计算增量 incX=dx/NincX = dx / NincY=dy/NincY = dy / N
  • (x,y)=(x0,y0)(x, y) = (x_0, y_0) 开始,循环 NN 次:
    • 绘制像素点 (round(x),round(y))(\mathrm{round}(x), \mathrm{round}(y))
    • x+=incXx += incXy+=incYy += incY

// DDA 算法实现

function drawLineDDA(x0, y0, x1, y1) {

// 1. 计算差分 dx 和 dy

let dx = x1 - x0,

dy = y1 - y0;

// 2. 确定步数 steps 为 max(|dx|, |dy|)

let steps = Math.max(Math.abs(dx), Math.abs(dy));

// 3. 计算增量 incX = dx / steps, incY = dy / steps

let incX = dx / steps,

incY = dy / steps;

// 4. 从起点开始,循环 steps 次,每次累加增量并绘制像素

let x = x0,

y = y0;

for (let i = 0; i <= steps; i++) {

ctx.fillRect(Math.round(x), Math.round(y), 1, 1); // 绘制像素点

x += incX;

y += incY;

}

}

// 优点:简单直观,易实现

// 缺点:浮点运算,开销大

  • 原理

    • 直线方程增量形式:以 xxyy 为主方向,每步累加微分增量
    • 设起点 (x0,y0)(x_0, y_0)、终点 (x1,y1)(x_1, y_1)Δx=x1−x0\Delta x = x_1 - x_0Δy=y1−y0\Delta y = y_1 - y_0
    • 取步数 N=max⁡(∣Δx∣,∣Δy∣)N = \max(|\Delta x|, |\Delta y|)
    • 增量: x∗inc=ΔxN,y∗inc=ΔyNx*{\text{inc}} = \frac{\Delta x}{N},\quad y*{\text{inc}} = \frac{\Delta y}{N}
    • 从起点出发,每步 x+=xincx += x_{\text{inc}}y+=yincy += y_{\text{inc}},并取整绘点
  • 特点

    • 简单直观,易于理解与实现
    • 浮点运算开销大,不利于硬件实现

4.2.2 中点 Bresenham 算法

算法原理

  • ∣dx∣≥∣dy∣|dx| \geq |dy| 时,以 xx 为主增量方向,初始误差 e=2⋅dy−dxe = 2 \cdot dy - dx
  • 每步 xx 增 1,同时根据误差调整 yy
    • 如果 e>0e > 0,则 yy 增 1,误差 e+=2⋅(dy−dx)e += 2 \cdot (dy - dx)
    • 否则 yy 不变,误差 e+=2⋅dye += 2 \cdot dy
  • 在每步绘制像素 (x,y)(x, y)

// Bresenham 算法实现

function drawLineBresenham(x0, y0, x1, y1) {

// 1. 计算 dx 和 dy 的绝对值

let dx = Math.abs(x1 - x0),

dy = Math.abs(y1 - y0);

// 2. 确定方向 sx 和 sy

let sx = x0 < x1 ? 1 : -1,

sy = y0 < y1 ? 1 : -1;

// 3. 初始化误差 err = dx - dy

let err = dx - dy;

// 4. 循环绘制像素,根据误差调整 x 和 y

let x = x0,

y = y0;

while (true) {

ctx.fillRect(x, y, 1, 1); // 绘制像素

if (x === x1 && y === y1) break;

let e2 = 2 * err;

if (e2 > -dy) {

err -= dy;

x += sx;

}

if (e2 < dx) {

err += dx;

y += sy;

}

}

}

// 5. 使用整数运算,避免浮点

// 优点:高效,纯整数运算

// 缺点:仅适用于 |dx| >= |dy| 的情况

  • 原理

    • 利用中点判别思想选择下一个像素,保持整数运算
    • 以斜率 m=ΔyΔx≤1m = \frac{\Delta y}{\Delta x} \leq 1 的情形为例:
      • 判别函数 d=f(x+1,y+12)=Δy(x+1)−Δx(y+12)d = f(x+1, y+\frac{1}{2}) = \Delta y (x+1) - \Delta x (y+\frac{1}{2})
      • 初始 d0=Δy−12Δxd_0 = \Delta y - \frac{1}{2} \Delta x
      • d<0d < 0,则下一个像素为 (x+1,y)(x+1, y),更新 d+=Δyd += \Delta y; 否则为 (x+1,y+1)(x+1, y+1),更新 d+=Δy−Δxd += \Delta y - \Delta x
  • 特点

    • 纯整数运算,无乘除法
    • 高效,适合硬件

4.2.3 改进 Bresenham 算法

  • 在中点算法基础上,进一步将判别式常量化,避免半像素累加

  • 令误差项 e=2Δy−Δxe = 2 \Delta y - \Delta x

  • 更新:

    • e<0e < 0e+=2Δye += 2 \Delta y
    • 否则:y+=1y += 1e+=2(Δy−Δx)e += 2 (\Delta y - \Delta x)
    • 每步 x+=1x += 1
  • 特点

    • 更简洁的整数累加方式
    • 广泛用于图形硬件

// 改进 Bresenham 算法实现

function drawLineImprovedBresenham(x0, y0, x1, y1) {

// 1. 初始化误差 e = 2*dy - dx

let dx = Math.abs(x1 - x0),

dy = Math.abs(y1 - y0);

let sx = x0 < x1 ? 1 : -1,

sy = y0 < y1 ? 1 : -1;

let e = 2 * dy - dx;

// 2. 循环 dx 次,每次 x 增 1

let x = x0,

y = y0;

for (let i = 0; i <= dx; i++) {

ctx.fillRect(x, y, 1, 1); // 绘制像素

// 3. 根据 e 判断是否调整 y

if (e < 0) {

e += 2 * dy;

} else {

y += sy;

e += 2 * (dy - dx);

}

// 4. 更新误差 e

x += sx;

}

}

// 优点:更简洁的整数累加,广泛用于硬件

// 缺点:需要处理方向


4.5 多边形扫描转换与区域填充

4.5.1 多边形表示

  • 顶点表示(Vertex List):存储顶点坐标和边连接顺序,适合几何变换。
  • 边界表示(Edge List):存储边的端点信息,便于扫描线求交。

4.5.2 X-扫描线填充算法(Basic Scanline Fill)

算法步骤

  1. 对多边形各边,根据边的 ymin 和 ymax 构建边表(Edge Table, ET)。
  2. 从 ymin 到 ymax 逐行扫描,维护有效边表(Active Edge Table, AET)。
  3. 在每条扫描线 y:
    • 将 ET[y] 中所有起始于 y 的边插入 AET,按照当前 x 坐标升序排序。
    • 从 AET 中删除所有 ymax == y 的边。
    • 对 AET 中每条边,根据当前 y 计算交点 x,并更新边的 x 增量(1/m)。
    • 将交点 x 值两两配对,形成填充区间,对区间内像素进行绘制。
  4. 更新每条边的 x 交点: x += Δx/Δy。

// 扫描线填充算法实现

function scanlineFill(poly) {

// 1. 构建边表 ET,按 ymin 分桶存储边信息

const ET = {};

for (let i = 0; i < poly.length; i++) {

const p1 = poly[i],

p2 = poly[(i + 1) % poly.length];

let ymin = Math.min(p1.y, p2.y),

ymax = Math.max(p1.y, p2.y);

if (ymin === ymax) continue;

let x = p1.y < p2.y ? p1.x : p2.x;

let invSlope = (p2.x - p1.x) / (p2.y - p1.y);

if (!ET[ymin]) ET[ymin] = [];

ET[ymin].push({ ymax, x, invSlope });

}

// 2. 初始化有效边表 AET

let AET = [];

const ymin = Math.min(...poly.map((p) => p.y));

const ymax = Math.max(...poly.map((p) => p.y));

// 3. 对每条扫描线 y:

for (let y = ymin; y <= ymax; y++) {

// - 插入新边到 AET

if (ET[y]) AET.push(...ET[y]);

// - 删除过期边

AET = AET.filter((edge) => edge.ymax > y);

// - 计算交点并排序

const intersections = AET.map((edge) => ({ x: edge.x })).sort((a, b) => a.x - b.x);

// - 两两配对填充区间

for (let i = 0; i < intersections.length; i += 2) {

const xStart = Math.ceil(intersections[i].x);

const xEnd = Math.floor(intersections[i + 1].x);

for (let x = xStart; x <= xEnd; x++) {

ctx.fillRect(x, y, 1, 1); // 填充像素

}

}

// - 更新边的 x 坐标

AET.forEach((edge) => (edge.x += edge.invSlope));

}

}

// 优点:高效,适合复杂多边形

// 缺点:需要预处理边表

4.3 圆的扫描转换

中点 Bresenham 画圆算法原理

  • 初始点 (x,y)=(0,R)(x, y) = (0, R),初始误差 d=1−Rd = 1 - R
  • 每步沿 xx 增 1,根据误差判断是否减 yy
    • 如果 d<0d < 0,则 d+=2x+3d += 2x + 3
    • 否则 y−=1y -= 1d+=2(x−y)+5d += 2(x - y) + 5
  • 在八分对称的 8 个位置绘制像素

// 中点 Bresenham 画圆算法实现

function drawCircleMidpoint(R, xc, yc) {

// 1. 初始化 x=0, y=R, d=1-R

let x = 0,

y = R;

let d = 1 - R;

// 2. 绘制八分对称点

plotCirclePoints(xc, yc, x, y);

// 3. 循环直到 x >= y:

while (x < y) {

// - 根据 d 判断是否减 y

if (d < 0) {

d += 2 * x + 3;

} else {

d += 2 * (x - y) + 5;

y--;

}

// - 更新 d

// - x++

x++;

// - 绘制对称点

plotCirclePoints(xc, yc, x, y);

}

}

function plotCirclePoints(xc, yc, x, y) {

const pts = [

[xc + x, yc + y],

[xc - x, yc + y],

[xc + x, yc - y],

[xc - x, yc - y],

[xc + y, yc + x],

[xc - y, yc + x],

[xc + y, yc - x],

[xc - y, yc - x],

];

pts.forEach((p) => ctx.fillRect(p[0], p[1], 1, 1));

}

// 优点:纯整数运算,高效

// 缺点:需要对称映射

4.3.1 八分对称绘制

  • 圆具有八次对称性,只需计算第一象限从 (R,0)(R,0)(R/2,R/2)(R/\sqrt2,R/\sqrt2) 的像素,其他象限对称映射

4.3.2 简单方程法

  • 利用圆的方程 x2+y2=R2x^2 + y^2 = R^2 或极坐标参数: x=Rcos⁡θ,y=Rsin⁡θx = R \cos \theta,\quad y = R \sin \theta
  • 缺点:需调用三角函数或开方,计算量大

4.3.3 中点 Bresenham 画圆

  • 判别函数: d=f(x+1,y−12)=(x+1)2+(y−12)2−R2d = f(x+1, y-\frac{1}{2}) = (x+1)^2 + (y-\frac{1}{2})^2 - R^2

  • 初始 d0=1−Rd_0 = 1 - R

  • 每步:

    • d<0d < 0:选择 (x+1,y)(x+1, y),更新 d+=2x+3d += 2x + 3
    • 否则:选择 (x+1,y−1)(x+1, y-1),更新 d+=2(x−y)+5d += 2(x - y) + 5
    • 同时 x+=1x += 1,视情况 y−=1y -= 1
  • 特点:纯整数累加,效率高


4.4 椭圆的扫描转换

算法原理

  • 椭圆标准方程:x2a2+y2b2=1\frac{x^2}{a^2} + \frac{y^2}{b^2} = 1,具有四象限对称性。
  • 使用中点 Bresenham 思想,将绘制分为两个区域:
    1. 区域1(斜率 ∣dy/dx∣≤1|dy/dx| \leq 1):以 xx 为主增量方向,判别函数 d1=b2(x+1/2)2+a2(y−1)2−a2b2d_1 = b^2 (x + 1/2)^2 + a^2 (y - 1)^2 - a^2 b^2
      • d1<0d_1 < 0,仅 x++x++,更新 d1+=b2(2x+1)d_1 += b^2 (2x + 1)
      • 否则 x++x++y−−y--,更新 d1+=b2(2x+1)+a2(−2y+1)d_1 += b^2 (2x + 1) + a^2 (-2y + 1)
    2. 区域2(斜率 ∣dy/dx∣>1|dy/dx| > 1):以 yy 为主变化方向,判别函数 d2=b2(x+1)2+a2(y−1/2)2−a2b2d_2 = b^2 (x + 1)^2 + a^2 (y - 1/2)^2 - a^2 b^2
      • d2>0d_2 > 0,仅 y−−y--,更新 d2+=a2(−2y+1)d_2 += a^2 (-2y + 1)
      • 否则 x++x++y−−y--,更新 d2+=b2(2x+2)+a2(−2y+1)d_2 += b^2 (2x + 2) + a^2 (-2y + 1)
  • 在每一步中,基于八象限对称性,绘制点 (±xc±x,±yc±y)(\pm x_c \pm x, \pm y_c \pm y)

// 中点 Bresenham 椭圆算法实现

function drawEllipseMidpoint(a, b, xc, yc) {

// 1. 初始化 x=0, y=b, 计算 a2, b2

let x = 0,

y = b;

let a2 = a * a,

b2 = b * b;

let d1 = b2 - a2 * b + 0.25 * a2;

let dx = 2 * b2 * x,

dy = 2 * a2 * y;

// 2. 区域1:斜率 <=1,以 x 为主

while (dx < dy) {

plotEllipsePoints(xc, yc, x, y);

// - 根据 d1 判断是否减 y

if (d1 < 0) {

x++;

dx += 2 * b2;

d1 += dx + b2;

} else {

x++;

y--;

dx += 2 * b2;

dy -= 2 * a2;

d1 += dx - dy + b2;

}

// - 更新 d1

}

// 3. 区域2:斜率 >1,以 y 为主

let d2 = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2;

while (y >= 0) {

plotEllipsePoints(xc, yc, x, y);

// - 根据 d2 判断是否增 x

if (d2 > 0) {

y--;

dy -= 2 * a2;

d2 += a2 - dy;

} else {

x++;

y--;

dx += 2 * b2;

dy -= 2 * a2;

d2 += dx - dy + a2;

}

// - 更新 d2

}

}

function plotEllipsePoints(xc, yc, x, y) {

const pts = [

[xc + x, yc + y],

[xc - x, yc + y],

[xc + x, yc - y],

[xc - x, yc - y],

];

pts.forEach((p) => ctx.fillRect(p[0], p[1], 1, 1));

}

// 4. 绘制四象限对称点

// 优点:整数运算,高效

// 缺点:复杂,需要两个区域


4.5 多边形扫描转换与区域填充

4.5.1 多边形表示

  • 顶点表示(Vertex List):存储顶点坐标和边连接顺序
  • 点阵表示(Bitmap):预先离散化边界,便于直接填充

4.5.2 X-扫描线填充

  • 对每条扫描线与多边形各边求交点
  • 将交点按 x 坐标排序,成对填充区间
  • 需处理顶点共享与交点重合特殊情况

4.5.3 有效边表算法(AET)

  • 边表(Edge Table,ET):按最小 y 值分桶存储所有边
  • 有效边表(Active Edge Table,AET):当前扫描线可见的边,按 x 递增排序
  • 算法流程:
    1. 初始化 ET 和空 AET
    2. 对每条扫描线 y:
      • 将 ET 中 y 为起点的边插入 AET
      • 从 AET 中删除扫描线终点已过的边
      • 对 AET 中每条边计算交点 x,并按 x 排序
      • 两两配对填充区间像素
      • 更新每条边的 x 增量(x+=1/mx += 1/m

4.5.4 区域填充算法

  • 边界填充(Edge Flag):遇到边界像素时切换填充状态
  • 种子填充(Seed Fill):从种子点扩展,递归或栈方式,四连通或八连通
  • 扫描线种子填充:按行扩展,减少栈深度和重复访问

4.5.5 内外测试规则

  • 奇偶规则(Even–Odd Rule):交点数为奇则内部
  • 非零环绕规则(Nonzero Winding Number):环绕计数非零则内部

4.6 字符处理

字符编码

  • ASCII(英文字符、控制码)
  • GB2312/GBK/UTF-8(中文及多语言)

字库类型

  • 点阵字库(Bitmap Font):按像素矩阵存储,简单快速但占用存储
  • 矢量字库(Vector Font):用线段、曲线描述笔画,支持缩放与变换

4.7 属性处理

线型与线宽

  • 线型:用像素模板(如 11110000)控制虚线、点划线
  • 线宽:
    • 刷子(Brush)算法:用矩形或圆形刷子覆盖线条
    • 区域填充:对扩展后的多边形边界进行填充

字符属性

  • 字体、字号、颜色、对齐方式(左/居中/右对齐)

区域填充属性

  • 填充颜色、图案、透明度
  • 图案对齐:全局对齐或局部对齐

4.8 反走样(Anti-Aliasing)

走样现象

  • 锯齿、波纹、闪烁、细节丢失

反走样方法

  1. 提高分辨率(Superresolution)
  2. 超采样(Supersampling):在高分辨率下采样并平均
  3. 区域采样(Area Sampling):按像素与图形覆盖区域比例加权
  4. 过滤器(Filter)方法:应用低通滤波器平滑边缘