第五章 二维图形的裁剪
- 掌握什么是裁剪、裁剪窗口,裁剪算法的基本内容
- 图形关于窗口区域内外关系的判别
- 图形与窗口的求交计算
- 掌握裁剪直线段的
Cohen-Sutherland
算法、中点分割算法 - 了解裁剪直线段的
Nicholl-Lee-Nicholl
算法 - 掌握裁剪多边形的
Sutherland-Hodgman
算法(又称逐边裁剪算法) - 了解裁剪多边形的
Weiler-Atherton
算法 - 掌握如何裁剪一个字符串,如何裁剪一个点阵表示(或矢量表示)的字符
裁剪
确定图形中哪些部分落在显示区之内,哪些落在显示区之外,以便只显示落在显示区内的那部分图形. 这个选择过程称为裁剪
裁剪的目的
判断图形元素是否落在裁剪窗口之内并找出其位于内部的部分
裁剪处理的基础
-
图元关于窗口内外关系的判别
-
图元与窗口的求交
直线段裁剪
假定条件
- 矩形裁剪窗口:
[xmin,xmax] × [ymin,ymax]
- 待裁剪直线段:
P0(x0,y0)
P1(x1,y1)
待裁剪线段和窗口的关系
- 线段完全可见, 如
AB
- 线段显然完全不可见, 如
EF
- 线段至少有一点在窗口外, 但非显然不可见, 如
GH
,IJ
,CD
- 需要进一步求交, 确定是否有可见部分, 并求出可见部分
因此在设计算法时, 应该考虑到
- 快速判断关系
1,2
- 减少关系
3
求交次数以及每次求交的计算量
直接求交算法
将直线与窗口边都协程参数形式, 然后直接用 判定点是否在多边形内部 的方法来判别直线端点与窗口的位置关系
算法步骤如下图所示
因为计算复杂, 一般不采用这种方式
Cohen-Sutherland
算法
算法步骤
- 判别线段两端点是否都落在窗口内
- 如果是,则线段完全可见
- 否则进入第二步
- 判别线段是否为显然不可见
- 如果是,则裁剪结束
- 否则进行第三步
- 求线段与窗口边延长线的交点,这个交点将线段分为两段,其中一段显然不可见,丢弃。对余下的另一段重新进行第一步,第二步判断,直至结束
如上图所示, 以线段
AD
为例
- A, D 不是都在窗口内, 进入第二步
- A 在窗口内, 不是显然不可见, 进入第三步
- 延长线交点 C, 丢弃 CD 段, 进入第一步
- A, C 不是都在窗口内, 进入第二步
- A 在窗口内, 不是显然不可见, 进入第三步
- 延长线交点 B, 丢弃 BC 段, 进入第一步
- A, B 都在窗口内, 裁剪结束
编码法
如何快速判断 完全可见 和 显然不可见, 提出编码法
编码方法:由窗口四条边所在直线把二维平面分成9个区域,每个区域赋予一个四位编码,CtCbCrCl
,上下右左
C
t
o
p
=
{
1
,
y
>
y
m
a
x
0
,
e
l
s
e
,
C
b
o
t
t
o
m
=
{
1
,
y
<
y
m
i
n
0
,
e
l
s
e
,
C
r
i
g
h
t
=
{
1
,
x
>
x
m
a
x
0
,
e
l
s
e
,
C
l
e
f
t
=
{
1
,
x
<
x
m
i
n
0
,
e
l
s
e
C_{top}=\begin{cases} 1 \text{ , } y > y_{max} \\ 0 \text{ , } else \end{cases} \text{ , } C_{bottom}=\begin{cases} 1 \text{ , } y < y_{min} \\ 0 \text{ , } else \end{cases} \text{ , } C_{right}=\begin{cases} 1 \text{ , } x > x_{max} \\ 0 \text{ , } else \end{cases} \text{ , } C_{left}=\begin{cases} 1 \text{ , } x < x_{min} \\ 0 \text{ , } else \end{cases}
Ctop={1 , y>ymax0 , else , Cbottom={1 , y<ymin0 , else , Cright={1 , x>xmax0 , else , Cleft={1 , x<xmin0 , else
平面被分为如下图的9个区域
-
完全可见的判别方法
- 两个端点的编码是否都为
0000
- 两个端点的编码是否都为
-
显然不可见的判别方法
- 线段的两个端点的编码的
逻辑“与”非零
- 线段的两个端点的编码的
-
对于那些非完全可见、又非显然不可见的线段
- 求交, 求交前需要先测试与窗口哪条边所在直线有交,这只要判断两个端点编码中各位的值
- 求交点的顺序固定为 左上右下
- 最坏情况: 需要求四个交点
例如之前提到的
AD
, 两端点编码分别为0000
,1001
, 因此与上, 左
两边有交点
- 先求出左侧交点
C
, 再求上侧交点B
如
EJ, 0101 | 1010
, 需要求4次交点, 按顺序分别是F, I, H, G
#define LEFT 1
#define RIGHT 2
#define BOTTOM 4
#define TOP 8
int encode(float x, float y, float XL, float XR, float YB, float YT) { // 计算点 x, y 的编码
int c = 0;
if (x < XL) c |= LEFT; // 置位 LEFT
if (x > XR) c |= RIGHT; // 置位 RIGHT
if (y < YB) c |= BOTTOM; // 置位 BOTTOM
if (y > YT) c |= TOP; // 置位 TOP
return c;
}
void CS_LineClip(float x1, float y1, float x2, float y2, float XL, float XR, float YB, float YT) {
int code1, code2, code;
int x, y;
code1 = encode(x1, y1, XL, XR, YB, YT);
code2 = encode(x2, y2, XL, XR, YB, YT); // 端点坐标编码
while (code1 != 0 || code2 != 0) { // 直到“完全可见”
if (code1 & code2 != 0) return; // 排除“显然不可见”情况
code = code1;
if (code1 == 0) code = code2; // 求得在窗口外的点
// 按顺序检测到端点的编码不为 0,才把线段与对应的窗口边界求交
if (code & LEFT) { // 线段与窗口左边延长线相交
x = XL;
y = y1 + (y2 - y1) * (XL - x1) / (x2 - x1);
} else if (code & RIGHT) { // 线段与窗口右边延长线相交
x = XR;
y = y1 + (y2 - y1) * (XR - x1) / (x2 - x1);
} else if (code & BOTTOM) { // 线段与窗口下边延长线相交
y = YB;
x = x1 + (x2 - x1) * (YB - y1) / (y2 - y1);
} else if (code & TOP) { // 线段与窗口上边延长线相交
y = YT;
x = x1 + (x2 - x1) * (YT - y1) / (y2 - y1);
}
if (code == code1) {
x1 = x; y1 = y;
code1 = encode(x, y, XL, XR, YB, YT); // 裁去 P1 到交点
} else {
x2 = x; y2 = y;
code2 = encode(x, y, XL, XR, YB, YT); // 裁去 P2 到交点
}
}
DrawLine(x1, y1, x2, y2);
}
中点算法
与Cohen-Sutherland算法一样,首先对线段端点进行编码,并把线段与窗口的关系分为三种情况
- 对前两种情况,进行一样的处理
- 对于第三种情况,用中点分割的方法求出线段与窗口的交点
- 从
P0
点出发找出距P0
最近的可见点 - 从
P1
点出发找出距P1
最近的可见点 - 两个可见点之间的连线即为线段
P0P1
的可见部分
- 从
- 对于第三种情况,用中点分割的方法求出线段与窗口的交点
从 P0 出发找最近可见点采用中点分割方法
计算出 P0P1 的中点 Pm
计算 P0 和 Pm 的区域码的位与
若结果等于0,说明最近可见点在 P0Pm 上,取 P0Pm 代替 P0P1
否则取 PmP1 代替 P0P1
如果 PmP1 的长度小于给定的容差,即|P1Pm|< ε,转步4;否则转步1
结果输出:Pm 就是 P0 的最近可见点,算法结束
该算法的主要计算过程只用到加法和除2运算,所以特别适合硬件实现
以下是中点算法的代码实现, 只写出了从P0出发找最近可见点采用中点分割方法, 实际中还需要从 P1 出发, 以及将数据改成整形类
void Mid_LineClip(float x1, float y1, float x2, float y2, float XL, float XR, float YB, float YT, float epsilon) {
int code1 = encode(x1, y1, XL, XR, YB, YT);
int code2 = encode(x2, y2, XL, XR, YB, YT);
if (code1 == 0 && code2 == 0) {
std::cout << x1 << ", " << y1 << " | " << x2 << ", " << y2 << std::endl; // 完全可见
return;
}
if ((code1 & code2) != 0) {
std::cout << "No visible part" << std::endl; // 显然不可见
return;
}
float xm, ym;
while (true) {
xm = (x1 + x2) / 2;
ym = (y1 + y2) / 2;
int code = encode(xm, ym, XL, XR, YB, YT);
if ((code1 & code) == 0) {
x2 = xm;
y2 = ym;
code2 = encode(x2, y2, XL, XR, YB, YT);
}
else {
x1 = xm;
y1 = ym;
code1 = encode(x1, y1, XL, XR, YB, YT);
}
if (std::hypot(x2 - x1, y2 - y1) < epsilon) {
break;
}
}
std::cout << xm << ", " << ym << std::endl; // 输出最近可见点
}
Nicholl-Lee-Nicholl
算法
- 消除C-S算法中多次求交的情况
- 对2D平面的更细的划分
假定待裁剪线段
P0P1
为非完全可见且非显然不可见
算法步骤
- 确定
P0
所在区域, 只考虑三种情况: 中, 左上, 左. 其他情况可以通过二维变换使P0
落在这三个区域里
- 落在
0000
区域- 落在
1001
区域- 落在
0001
区域- 从
P0
点向窗口的四个角点发出射线,这四条射线和窗口的四条边所在的直线一起将二维平面划分为更多的小区域
- 确定P1所在的区域 (判断P1所在区域位置,可判定P0、P1与窗口那条边求交)
- 根据窗口四边的坐标值及
P0P1
和各射线的斜率可确定P1所在的区域- 求交点,确定
P0P1
的可见部分
- 效率较高,但仅适合二维矩形窗口
- 可以不会, 但我还是手搓了一个, 对于二维变换的部分这里没有涉及, 只写了上图四种情况
// 判断点与直线位置函数
bool PositionToLine(float px, float py, float p0x, float p0y, float p1x, float p1y) {
float F = (p1x - p0x) * (py - p0y) - (p1y - p0y) * (px - p0x);
return F >= 0; // 返回点是否在直线上方
}
void NLN_LineClip(float x1, float y1, float x2, float y2, float XL, float XR, float YB, float YT) {
int code1 = encode(x1, y1, XL, XR, YB, YT);
int code2 = encode(x2, y2, XL, XR, YB, YT);
if (code1 == 0 && code2 == 0) {
std::cout << x1 << ", " << y1 << " | " << x2 << ", " << y2 << std::endl; // 完全可见
return;
}
if ((code1 & code2) != 0) {
std::cout << "No visible part" << std::endl; // 显然不可见
return;
}
// 计算从 P0 到各角的斜率
float kLB = (y1 - YB) / (x1 - XL);
float kLT = (y1 - YT) / (x1 - XL);
float kRB = (y1 - YB) / (x1 - XR);
float kRT = (y1 - YT) / (x1 - XR);
float k01 = (y1 - y2) / (x1 - x2);
// P0 在可见区域
if (code1 == 0) {
std::cout << "P0可见,求P1交点\n";
if (x2 < XL && k01 <= kLB && k01 >= kLT) { // P1 在 L 区域
std::cout << "区域L\n";
float intersectionX = XL;
float intersectionY = k01 * (intersectionX - x1) + y1;
std::cout << x1 << ", " << y1 << " | " << intersectionX << ", " << intersectionY << std::endl; // 输出可见部分
return;
}
else if (x2 > XR && k01 >= kRB && k01 <= kRT) { // P1 在 R 区域
std::cout << "区域R\n";
float intersectionX = XR;
float intersectionY = k01 * (intersectionX - x1) + y1;
std::cout << x1 << ", " << y1 << " | " << intersectionX << ", " << intersectionY << std::endl; // 输出可见部分
return;
}
else if (y2 < YB && (k01 > kLB || k01 < kRB)) { // P1 在 B 区域
std::cout << "区域B\n";
float intersectionY = YB;
float intersectionX = (intersectionY - y1) / k01 + x1;
std::cout << x1 << ", " << y1 << " | " << intersectionX << ", " << intersectionY << std::endl; // 输出可见部分
return;
}
else if (y2 > YT && (k01 < kLT || k01 > kRT)) { // P1 在 T 区域
std::cout << "区域T\n";
float intersectionY = YT;
float intersectionX = (intersectionY - y1) / k01 + x1;
std::cout << x1 << ", " << y1 << " | " << intersectionX << ", " << intersectionY << std::endl; // 输出可见部分
return;
}
}
// P0 在左侧区域
if (code1 == LEFT) {
std::cout << "P1在左侧,求P1交点\n";
// 求 P0 与左侧边框交点
float intersectionX0 = XL;
float intersectionY0 = k01 * (intersectionX0 - x2) + y2;
// 根据 P1 位置求交点
if (code2 == 0) { // P1 可见 ( 在 L 区域 )
std::cout << "区域L\n";
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << x2 << ", " << y2 << std::endl; // 输出可见部分
return;
}
else if (k01 <= kLT && k01 >= kRT) { // P1 在 LT 区域
std::cout << "区域LT\n";
float intersectionY1 = YT;
float intersectionX1 = (intersectionY1 - y1) / k01 + x1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
else if (k01 < kRT && k01 >= kRB) { // P1 在 LR 区域
std::cout << "区域LR\n";
float intersectionX1 = XR;
float intersectionY1 = k01 * (intersectionX1 - x1) + y1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
else if (k01 < kRB && k01 >= kLB) { // P1 在 LB 区域
std::cout << "区域LB\n";
float intersectionY1 = YB;
float intersectionX1 = (intersectionY1 - y1) / k01 + x1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
}
// P0 在左上角的下半部分
if (code1 == LEFT + TOP && !PositionToLine(x1, y1, XL, YT, XR, YB)) {
std::cout << "P1在左上侧的对角线下半部分,求P1交点\n";
if (k01 <= kRT && k01 >= kLT) {
// 求 P0 与上侧边框交点
float intersectionY0 = YT;
float intersectionX0 = (intersectionY0 - y2) / k01 + x2;
if (code2 == 0) {
std::cout << "区域T\n";
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << x2 << ", " << y2 << std::endl; // 输出可见部分
return;
}
else {
std::cout << "区域TR\n";
float intersectionX1 = XR;
float intersectionY1 = k01 * (intersectionX1 - x1) + y1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
}
else if (k01>= kLB && k01 < kLT) {
// 求 P0 与左侧边框交点
float intersectionX0 = XL;
float intersectionY0 = k01 * (intersectionX0 - x2) + y2;
if (code2 == 0) {
std::cout << "区域L\n";
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << x2 << ", " << y2 << std::endl; // 输出可见部分
return;
}
else if (k01 >= kRB) {
std::cout << "区域LR\n";
float intersectionX1 = XR;
float intersectionY1 = k01 * (intersectionX1 - x1) + y1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
else if (k01 >= kLB) {
std::cout << "区域LB\n";
float intersectionY1 = YB;
float intersectionX1 = (intersectionY1 - y1) / k01 + x1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
}
return;
}
// P0 在左上角的上半部分
if (code1 == LEFT + TOP && PositionToLine(x1, y1, XL, YT, XR, YB)) {
std::cout << "P1在左上侧的对角线上半部分,求P1交点\n";
if (k01 <= kRT && k01 >= kLT) {
// 求 P0 与上侧边框交点
float intersectionY0 = YT;
float intersectionX0 = (intersectionY0 - y2) / k01 + x2;
if (code2 == 0) {
std::cout << "区域T\n";
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << x2 << ", " << y2 << std::endl; // 输出可见部分
return;
}
else if (k01 >= kRB){
std::cout << "区域TR\n";
float intersectionX1 = XR;
float intersectionY1 = k01 * (intersectionX1 - x1) + y1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
else if (k01 >= kLT) {
std::cout << "区域TB\n";
float intersectionY1 = YB;
float intersectionX1 = (intersectionY1 - y1) / k01 + x1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
}
else if (k01 >= kLB && k01 < kLT) {
// 求 P0 与左侧边框交点
float intersectionX0 = XL;
float intersectionY0 = k01 * (intersectionX0 - x2) + y2;
if (code2 == 0) {
std::cout << "区域L\n";
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << x2 << ", " << y2 << std::endl; // 输出可见部分
return;
}
else {
std::cout << "区域LB\n";
float intersectionY1 = YB;
float intersectionX1 = (intersectionY1 - y1) / k01 + x1;
std::cout << intersectionX0 << ", " << intersectionY0 << " | " << intersectionX1 << ", " << intersectionY1 << std::endl; // 输出可见部分
return;
}
}
return;
}
}