第五章 二维图形的裁剪
- 掌握什么是裁剪、裁剪窗口,裁剪算法的基本内容
- 图形关于窗口区域内外关系的判别
- 图形与窗口的求交计算
- 掌握裁剪直线段的
Cohen-Sutherland
算法、中点分割算法 - 了解裁剪直线段的
Nicholl-Lee-Nicholl
算法 - 掌握裁剪多边形的
Sutherland-Hodgman
算法(又称逐边裁剪算法) - 了解裁剪多边形的
Weiler-Atherton
算法 - 掌握如何裁剪一个字符串,如何裁剪一个点阵表示(或矢量表示)的字符
多边形裁剪
多边形是一个封闭区域,裁剪后仍应当是封闭的多边形
- 需要用窗口边的适当部分来封闭它
Sutherland_Hodgman
算法
算法思想
将多边形关于矩形窗口的裁剪分解为多边形关于窗口四边所在直线的裁剪
按照 左上右下 的顺序裁剪, 实现是一个 流水线过程, 即上一步的输出是上一步的输入, 流程如下图
考虑窗口的边以及延长线构成的裁剪线,该线把平面分成两个部分
- 一部分包含窗口,称为内侧空间
- 另一部分称为外侧空间
依序考虑多边形的各条边的两端点S,P:它们与裁剪线的位置关系只有四种
- S,P均在内侧
- S,P均在外侧
- S在内侧,P在外侧
- S在外侧,P在内侧
算法步骤
- 假设现在处理的多边形边是SP,端点S在上一轮处理过了. 线段端点S、P与裁剪线比较之后,可输出0至2个顶点
情况(1)
仅输出顶点P
情况(2)
输出0个顶点情况(3)
输出线段SP与裁剪线的交点 I
情况(4)
输出线段SP与裁剪线的交点 I
和顶点P
裁剪结果的顶点集由两部分构成
- 裁剪边内侧的原顶点
- 多边形的边与裁剪边的交点
- 顺序连接新顶点集
优点
- 裁剪算法采用流水线方式,适合硬件实现
- 可推广到任意凸多边形裁剪窗口
适用范围
- 适用于裁剪凸多边形
- 对于凹多边形,如果裁剪后的多边形只有一个,结果仍然是正确的;但是如果裁剪后的多边形有分离部分出现,即结果多边形多于一个,这时会出现多余的线
- 将凹多边形分割成2个或多个凸多边形分别进行处理
- 对于凹多边形,如果裁剪后的多边形只有一个,结果仍然是正确的;但是如果裁剪后的多边形有分离部分出现,即结果多边形多于一个,这时会出现多余的线
typedef struct {
float x;
float y;
} Vertex; // 顶点
typedef Vertex Edge[2]; // 边
typedef Vertex VertexArray[MAX]; // 多边形顶点数组
bool IsOutLine(Vertex p, Edge ClipBoundary) {
// 判断顶点是否在左边界或下边界外部
if (ClipBoundary[0].x == ClipBoundary[1].x) {
return p.x >= ClipBoundary[0].x;
}
else {
return (ClipBoundary[1].x - ClipBoundary[0].x) * (p.y - ClipBoundary[0].y) -
(ClipBoundary[1].y - ClipBoundary[0].y) * (p.x - ClipBoundary[0].x) >= 0;
}
}
bool IsInLine(Vertex p, Edge ClipBoundary) {
// 判断顶点是否在右边界或上边界内部
if (ClipBoundary[0].x == ClipBoundary[1].x) {
return p.x < ClipBoundary[0].x;
}
else {
return (ClipBoundary[1].x - ClipBoundary[0].x) * (p.y - ClipBoundary[0].y) -
(ClipBoundary[1].y - ClipBoundary[0].y) * (p.x - ClipBoundary[0].x) < 0;
}
}
// 计算交点
void IntersectPoint(Vertex s, Vertex p, Edge ClipBoundary, Vertex& ip) {
float dx = p.x - s.x;
float dy = p.y - s.y;
float x0 = ClipBoundary[0].x;
float y0 = ClipBoundary[0].y;
float x1 = ClipBoundary[1].x;
float y1 = ClipBoundary[1].y;
float slopeBoundary = (y1 - y0) / (x1 - x0);
if (x1 - x0 == 0) { // 竖直边界
ip.x = x0;
if (dx != 0) { // 检查防止除以0
float m = dy / dx;
float c = s.y - m * s.x;
ip.y = m * x0 + c;
}
else {
ip.y = s.y; // 处理竖直线段的情况
}
}
else if (y1 - y0 == 0) { // 水平边界
ip.y = y0;
if (dx != 0) {
float m = dy / dx;
float c = s.y - m * s.x;
ip.x = (y0 - c) / m;
}
else {
ip.x = s.x; // 处理竖直线段的情况
}
}
else {
if (dx != 0) {
float m = dy / dx;
float c = s.y - m * s.x;
ip.x = (c - y0 + slopeBoundary * x0) / (slopeBoundary - m);
ip.y = m * ip.x + c;
}
else {
ip.x = s.x; // 处理竖直线段的情况
ip.y = slopeBoundary * (s.x - x0) + y0;
}
}
}
// 输出点到结果数组
void Output(Vertex p, int& OutLength, VertexArray OutVertexArray) {
OutVertexArray[OutLength++] = p;
}
// Sutherland-Hodgman裁剪算法实现
void SutherlandHodgmanClip(VertexArray InVertexArray, VertexArray OutVertexArray, Edge ClipBoundary, int& InLength, int& OutLength, bool (*DecisionFunction)(Vertex, Edge)) {
Vertex s, p, ip;
OutLength = 0;
s = InVertexArray[InLength - 1]; // 处理第一个S点
for (int j = 0; j < InLength; j++) {
p = InVertexArray[j]; // 当前处理的P点
// 判断S点和P点是否在裁剪线内侧
bool sIn = DecisionFunction(s, ClipBoundary);
bool pIn = DecisionFunction(p, ClipBoundary);
if (pIn) {
if (sIn) { // 情况1
Output(p, OutLength, OutVertexArray);
}
else { // 情况4
IntersectPoint(s, p, ClipBoundary, ip);
Output(ip, OutLength, OutVertexArray);
Output(p, OutLength, OutVertexArray);
}
}
else if (sIn) { // 情况3
IntersectPoint(s, p, ClipBoundary, ip);
Output(ip, OutLength, OutVertexArray);
}
// 情况4无需处理
s = p; // 当前P点成为下一个S点
}
}
// 执行完整的矩形窗口裁剪过程
void SutherlandHodgmanPolygonClip(VertexArray InVertexArray, VertexArray OutVertexArray, int& InLength, int& OutLength, Vertex lowerLeft, Vertex upperRight) {
Edge ClipBoundary;
VertexArray tempArray1, tempArray2;
int tempLength1, tempLength2;
// 左边界
ClipBoundary[0] = { lowerLeft.x, lowerLeft.y };
ClipBoundary[1] = { lowerLeft.x, upperRight.y };
SutherlandHodgmanClip(InVertexArray, tempArray1, ClipBoundary, InLength, tempLength1, IsOutLine);
// 右边界
ClipBoundary[0] = { upperRight.x, lowerLeft.y };
ClipBoundary[1] = { upperRight.x, upperRight.y };
SutherlandHodgmanClip(tempArray1, tempArray2, ClipBoundary, tempLength1, tempLength2, IsInLine);
// 下边界
ClipBoundary[0] = { lowerLeft.x, lowerLeft.y };
ClipBoundary[1] = { upperRight.x, lowerLeft.y };
SutherlandHodgmanClip(tempArray2, tempArray1, ClipBoundary, tempLength2, tempLength1, IsOutLine);
// 上边界
ClipBoundary[0] = { lowerLeft.x, upperRight.y };
ClipBoundary[1] = { upperRight.x, upperRight.y };
SutherlandHodgmanClip(tempArray1, OutVertexArray, ClipBoundary, tempLength1, OutLength, IsInLine);
}
Weiler-Athenton
算法
有时需考虑裁剪窗口为任意多边形(凸、凹、带内环)的情况
算法思想
- 主多边形:被裁剪多边形,记为
A
- 裁剪多边形:裁剪窗口,记为
B
- 主多边形和裁剪多边形把二维平面分成四部分
- 内裁剪(图元在窗口内的部分):
A∩B
- 外裁剪(图元在窗口外的部分):
A-B
- 内裁剪(图元在窗口内的部分):
- 主多边形和裁剪多边形把二维平面分成四部分
- 裁剪结果区域的边界由
A
的部分边界和B
的部分边界两部分构成,并且在交点处边界发生交替,即由A
的边界转至B
的边界,或由B
的边界转至A
的边界 - 约定
- 外环 (由多边形的外部边界构成) 顶点编号取逆时针方向
- 内环 (由多边形的内部边界构成) 顶点编号取顺时针方向
由于多边形的封闭性,如果主多边形与裁剪多边形有交点,则交点成对出现,它们被分为如下两类
进点:主多边形边界由此进入裁剪多边形
- 如:I1,I3, I5, I7, I9, I11
出点:主多边形边界此离开裁剪多边形区域
- 如:I0,I2, I4, I6, I8, I10
算法步骤
- 建立主多边形和裁剪多边形的顶点表
- 求主多边形和裁剪多边形的交点,并将这些交点按顺序插入两多边形的顶点表中. 在两多边表形顶点表中的相同交点间建立双向指针
- 裁剪: 如果存在没有被跟踪过的交点,执行以下步骤:
- 建立空的裁剪结果多边形的顶点表
- 选取任一没有被跟踪过的交点为始点,将其输出到结果多边形顶点表中
- 如果该交点为进点,跟踪主多边形边边界;否则跟踪裁剪多边形边界
- 跟踪多边形边界,每遇到多边形顶点,将其输出到结果多边形顶点表中,直至遇到新的交点
- 将该交点输出到结果多边形顶点表中,并通过连接该交点的双向指针改变跟踪方向(如果上一步跟踪的是主多边形边界,现在改为跟踪裁剪多边形边界;如果上一步跟踪裁剪多边形边界,现在改为跟踪主多边形边界)
- 重复(4)、(5)直至回到起点
特殊情况
- 与裁剪多边形边重合的主多边形的边不参与求交点
- 顶点处理:对于顶点落在裁剪多边形的边上的主多边形的边,如果落在该裁剪边的内侧,将该顶点算作交点;而如果这条边落在该裁剪边的外侧,将该顶点不看作交点
- 可以处理凹多边形的裁剪
- 可以生成多个裁剪结果多边形
字符裁剪
当字符和文本部分在窗口内,部分在窗口外时,就提出了字符裁剪问题
策略选择有:
- 基于字符串
- 基于字符
- 基于构成字符的最小元素
- 点阵字符-点裁剪
- 矢量字符-线裁剪