计算机图形学-基本图形生成算法
发布时间:
更新时间:
🕒 阅读时间:22 min read
👀 阅读量:Loading...
第四章 基本图形生成算法
4.1 引言
问题提出 在光栅设备(如显示器、打印机)上如何生成基本二维图形(点、直线、圆、椭圆、多边形、字符等)?
图形生成(扫描转换) 将连续的几何描述转换为离散像素集合,即在数字设备上确定哪些像素最能逼近目标图形。
关键目标
- 保持图形逼真度
- 端点和轮廓准确
- 颜色与灰度均匀
- 算法执行效率高
- 支持绘制属性(颜色、线型、线宽等)
4.2 直线的扫描转换
4.2.1 数值微分法(DDA 算法)
算法原理
- 给定起点 和终点 ,计算差分 ,
- 确定步数
- 计算增量 ,
- 从 开始,循环 次:
- 绘制像素点
- ,
// 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; }}// 优点:简单直观,易实现// 缺点:浮点运算,开销大
-
原理
- 直线方程增量形式:以 或 为主方向,每步累加微分增量
- 设起点 、终点 ,,
- 取步数
- 增量:
- 从起点出发,每步 ,,并取整绘点
-
特点
- 简单直观,易于理解与实现
- 浮点运算开销大,不利于硬件实现
4.2.2 中点 Bresenham 算法
算法原理
- 当 时,以 为主增量方向,初始误差
- 每步 增 1,同时根据误差调整 :
- 如果 ,则 增 1,误差
- 否则 不变,误差
- 在每步绘制像素
// 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| 的情况
-
原理
- 利用中点判别思想选择下一个像素,保持整数运算
- 以斜率 的情形为例:
- 判别函数
- 初始
- 若 ,则下一个像素为 ,更新 ; 否则为 ,更新 。
-
特点
- 纯整数运算,无乘除法
- 高效,适合硬件
4.2.3 改进 Bresenham 算法
-
在中点算法基础上,进一步将判别式常量化,避免半像素累加
-
令误差项
-
更新:
- 若 :;
- 否则:,;
- 每步
-
特点
- 更简洁的整数累加方式
- 广泛用于图形硬件
// 改进 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)
算法步骤:
- 对多边形各边,根据边的 ymin 和 ymax 构建边表(Edge Table, ET)。
- 从 ymin 到 ymax 逐行扫描,维护有效边表(Active Edge Table, AET)。
- 在每条扫描线 y:
- 将 ET[y] 中所有起始于 y 的边插入 AET,按照当前 x 坐标升序排序。
- 从 AET 中删除所有 ymax == y 的边。
- 对 AET 中每条边,根据当前 y 计算交点 x,并更新边的 x 增量(1/m)。
- 将交点 x 值两两配对,形成填充区间,对区间内像素进行绘制。
- 更新每条边的 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 画圆算法原理
- 初始点 ,初始误差
- 每步沿 增 1,根据误差判断是否减 :
- 如果 ,则
- 否则 ,
- 在八分对称的 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 八分对称绘制
- 圆具有八次对称性,只需计算第一象限从 到 的像素,其他象限对称映射
4.3.2 简单方程法
- 利用圆的方程 或极坐标参数:
- 缺点:需调用三角函数或开方,计算量大
4.3.3 中点 Bresenham 画圆
-
判别函数:
-
初始
-
每步:
- 若 :选择 ,更新 ;
- 否则:选择 ,更新 ;
- 同时 ,视情况
-
特点:纯整数累加,效率高
4.4 椭圆的扫描转换
算法原理
- 椭圆标准方程:,具有四象限对称性。
- 使用中点 Bresenham 思想,将绘制分为两个区域:
- 区域1(斜率 ):以 为主增量方向,判别函数
。
- 若 ,仅 ,更新 ;
- 否则 ,,更新 。
- 区域2(斜率 ):以 为主变化方向,判别函数
。
- 若 ,仅 ,更新 ;
- 否则 ,,更新 。
- 区域1(斜率 ):以 为主增量方向,判别函数
。
- 在每一步中,基于八象限对称性,绘制点 。
// 中点 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 递增排序
- 算法流程:
- 初始化 ET 和空 AET
- 对每条扫描线 y:
- 将 ET 中 y 为起点的边插入 AET
- 从 AET 中删除扫描线终点已过的边
- 对 AET 中每条边计算交点 x,并按 x 排序
- 两两配对填充区间像素
- 更新每条边的 x 增量()
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)
走样现象
- 锯齿、波纹、闪烁、细节丢失
反走样方法
- 提高分辨率(Superresolution)
- 超采样(Supersampling):在高分辨率下采样并平均
- 区域采样(Area Sampling):按像素与图形覆盖区域比例加权
- 过滤器(Filter)方法:应用低通滤波器平滑边缘
计算机图形学-基本图形生成算法
本文链接: https://xingwangzhe.fun/posts/10deed37
本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
留言评论