计算机图形学——Weilerr_Atherton裁剪算法
前提:假设多边形与裁剪框的顶点均是有序存储,且最终是首位相连形成闭合图形。
适合情景:多变裁剪多边,可裁剪凹、凸等任意多边形。
不适合:线的裁剪、圆的裁剪。
Weilerr_Atherton(双边裁剪)的基本思想:
根据多边形的处理方向(顺时针/逆时针)和当前处理的多边形顶点对是由外到内,还是由内到外来确定裁剪后多边形的顶点连接方式:
- 沿着多边形边界方向连接
- 沿着裁剪框边界方向连接
注:此处的多边形边界方向指的是多边形顶点存储的顺序,是按照逆时针还是顺时针存储。
如果是顺时针处理多边形顶点,则采用以下规则:
- 对由内到外的顶点对(此时多边形与裁剪框的交点为进点),沿着多边形边界的方向连接。
- 对由外到内的顶点(此时多边形与裁剪框的交点为出点),按顺时针沿着裁剪框的边界连接。
算法的实现步骤:
- 预先处理多边形与裁剪框的顶点连接顺序。(只有当二者顶点均是按照同一个方向存储连接的才可以使用该算法)
- 计算多边形与裁剪框的交点,并将交点依照顺序插入多边形与裁剪框的顶点序列中。
- 建立交点索引,两个多边形的相同顶点各自指向对方顶点的位置。
- 选取任意一个交点为起点,将其保存到裁剪结果顶点表中。
- 如果该交点为进点,跟踪多边形边界,并将其保存到裁剪结果顶点表中。
- 否则,跟踪裁剪框边界。
- 跟踪图形的边界,每遇到顶点,将其保存到裁剪结果的顶点表中,直至遇见新的交点。
注:如果遇见相同的交点,一种是裁剪结果中有多个封闭图形,一种是顶点裁剪结束。- 将该顶点输出到裁剪结果的顶点表中,并通过建立的索引改变跟踪的图形边界。
- 如果上一步跟踪的为多边形边界,则改为跟踪裁剪框边界。反之跟踪多边形边界。
- 重复5、6直到所有的交点遍历过一次。
算法的代码实现:
- 多边形与裁剪框的预处理,令二者的顶点连接顺序都是按照同一方向(逆时针或顺时针)。
- 采用原理:二维多边形的方向面积,即每个顶点看作一个向量,按照顶点顺序,依次进行叉积。如果叉积结果大于零为逆时针,小于零顺时针,等于零共线。
#include<iostream>
#include<vector>
#include <algorithm>
using namespace std;
/// <summary>
/// 判断面积的符号,有向面积法
///area > 0 "逆时针";
///area < 0 "顺时针";
///area=0 "共线";
/// </summary>
/// <param name="vertices">图形顶点</param>
/// <returns>返回有向面积的数值</returns>
double checkPolsecondgonOrientation(const vector<pair<int, int>>& vertices) {
int n = vertices.size();
double area = 0;
// 计算有向面积
for (int i = 0; i < n; ++i) {
const pair<int, int>& p1 = vertices[i];
const pair<int, int>& p2 = vertices[(i + 1) % n]; // 下一个顶点(闭合)
area += (p1.first * p2.second - p2.first * p1.second);
}
return area;
}
/// <summary>
/// 初始化调整裁剪框与图形的顶点顺序为相同
/// </summary>
/// <param name="frame">裁剪框顶点</param>
/// <param name="picture">图形顶点</param>
void Initializing_Graphics(vector<pair<int, int>>& frame, vector<pair<int, int>>& picture) {
// 判断多边形顶点顺序
double pictureflage = checkPolsecondgonOrientation(picture);
double frameflage = checkPolsecondgonOrientation(frame);
if (pictureflage * frameflage < 0) {
reverse(frame.begin(), frame.end());//反转
}
}
- 计算多边形与裁剪框的交点
- 特殊点处理:
- 二者顶点重合,不计算
- 裁剪框的顶点在多边形的边上,将裁剪框的顶点插入多边形。
- 多边形的顶点在裁剪框的边上,将多边形的顶点插入多边形。
- 交点的计算:
- 使用直线的参数方程 ( x , y ) = ( x 0 , y 0 ) + t ( x 1 − x 0 , y 1 − y 0 ) (x,y)=(x_{0},y_{0})+t(x_{1}-x_{0},y_{1}-y_{0}) (x,y)=(x0,y0)+t(x1−x0,y1−y0)
- 分别建立两个边所在直线的参数方程,联立求解参数,参数分别为 t 1 、 t 2 t_{1}、t_{2} t1、t2。
- 判断交点是否为两个边(线段)的交点
- 当 0 < = t < = 1 0<=t<=1 0<=t<=1时,表示该点在这个线段上(包括两个端点)
- 特殊点处理:
/// <summary>
/// 计算两个直线的交点参数,使用直线的参数方程,A为定点
/// </summary>
/// <param name="A0">直线1的点A</param>
/// <param name="B0">直线1的点B</param>
/// <param name="C0">直线2的点A</param>
/// <param name="D0">直线2的点B</param>
/// <returns>依次返回直线1、2的参数</returns>
vector<double> Cal_intersec_straight_lines(const pair<int, int>& A0, const pair<int, int>& B0, const pair<int, int>& C0, const pair<int, int>& D0) {
pair<double, double> A = { static_cast<double>(A0.first),static_cast<double>(A0.second) };
pair<double, double> B = { static_cast<double>(B0.first),static_cast<double>(B0.second) };
pair<double, double> C = { static_cast<double>(C0.first),static_cast<double>(C0.second) };
pair<double, double> D = { static_cast<double>(D0.first),static_cast<double>(D0.second) };
//判断是否平行
vector<double> result;
pair<double, double> AB = { B.first - A.first,B.second - A.second };
pair<double, double> CD = { D.first - C.first,D.second - C.second };
//计算t,u
double denom = AB.first * CD.second - AB.second * CD.first;
if (denom == 0) {
return result;
}
double t_up = (C.first - A.first) * (D.second - C.second) - (D.first - C.first) * (C.second - A.second);
double t_num = t_up / denom;
double u_up = (C.first - A.first) * (B.second - A.second) - (B.first - A.first) * (C.second - A.second);
double u_num = u_up / denom;
result.push_back(t_num);
result.push_back(u_num);
return result;
}
/// <summary>
/// 两个图形的交点,并按照顺序插入
/// </summary>
/// <param name="Frame">裁剪框的顶点</param>
/// <param name="Picture">图形的顶点</param>
void Graphical_Intersection(vector<pair<int, int>>& Frame, vector<pair<int, int>>& Picture) {
int fnum = Frame.size(), pnum = Picture.size();
for (int i = 0; i < fnum; i++) {
for (int j = 0; j < pnum; j++) {
vector<double> node = Cal_intersec_straight_lines(Frame[i], Frame[(i + 1) % fnum], Picture[j], Picture[(j + 1) % pnum]);
if (!node.empty()) {
pair<int, int> exp = { static_cast<int>(round(Frame[i].first + node[0] * (Frame[(i + 1) % fnum].first - Frame[i].first))),static_cast<int>(round(Frame[i].second + node[0] * (Frame[(i + 1) % fnum].second - Frame[i].second))) };
if (0 < node[0] && node[0] < 1&& 0 <= node[1] && node[1] <= 1) {
//将交点插入裁剪框
Frame.insert(Frame.begin() + i + 1, exp);
fnum++;
}
if (0 <= node[0] && node[0] <= 1 && 0 < node[1] && node[1] < 1) {
//将交点插入图形
Picture.insert(Picture.begin() + j + 1, exp);
pnum++;
}
}
}
}
}
- 最终Weilerr_Atherton算法的书写
/// <summary>
///判断该点是否为进点
/// </summary>
/// <param name="Frame">裁剪框的顶点</param>
/// <param name="A">交点</param>
/// <param name="B">交点对应的图形的下一顶点</param>
/// <returns>true表示是进点</returns>
bool isInNode(const vector<pair<int, int>>& Frame, pair<int, int>A, pair<int, int>B) {
vector<double> node;
int fnum = Frame.size();
for (int i = 0; i < fnum; i++) {
node = Cal_intersec_straight_lines(Frame[i], Frame[(i + 1) % fnum], A, B);
if (!node.empty()) {
if (0 <= node[0] && node[0] <= 1 && node[1] > 0) {//边上有交点,并且在射线上
return true;
}
}
}
return false;
}
// <summary>
/// 裁剪算法
/// </summary>
/// <param name="Frame">裁剪框顶点</param>
/// <param name="Picture">图形顶点</param>
/// <returns>返回裁剪后的顶点,每一个封闭图形是一个单独的vector<pair<int,int>>容器</returns>
vector<vector<pair<int, int>>> Weiler_Atherton(vector<pair<int, int>>& Frame, vector<pair<int, int>>& Picture) {
vector<vector<pair<int, int>>> result;
if (result.size() == 0) {
result.push_back({});
}
//处理图形与裁剪图形,他们的顶点连接方向应该相反
Initializing_Graphics(Frame, Picture);
//计算两个图形的交点,并按照顺序插入两个图形
Graphical_Intersection(Frame, Picture);
//建立索引表
//index.first为Frame的索引
//index.second为Picture的索引
vector<pair<int, int>> index;
for (int i = 0; i < Frame.size(); i++) {//沿着裁剪框的边界方向建立索引
for (int j = 0; j < Picture.size(); j++) {
if (Frame[i] == Picture[j]) {
index.emplace_back(i, j);
break;//交点只有一个,所以找到合适的立马退出就行
}
}
}
//索引建立后开始创建图案表
//什么时候结束?
//任意取一个交点,判断交点类型
// 进点沿着图形的顺序保存,出点沿着裁剪框的顺序保存,直到遇见的新的交点
//什么时候图形结束?再次遇见刚开始的交点。
//什么时候所有图形结束?
//交点其实是按照顺序存储的,只是每次旋转着,就会回到起点
//因此,在遇见一个闭环时,按照顺序查找下一交点
int index_num = index.size();
int picture_num = Picture.size();
int count = 0;
//裁剪后图形顶点的存储
int flage = isInNode(Frame, Frame[index[0].first], Picture[(index[0].second + 1) % picture_num]);
for (int i = 0; i < index_num; i++) {
if (flage) {//进点,跟着图形走
int j = index[i].second;
while (true){
result[count].push_back(Picture[j % picture_num]);//存储图形顶点
j++;
auto it = find_if(index.begin(), index.end(), [j,picture_num](const pair<int, int>& a) {return a.second == j%picture_num; });
if (it != index.end()) {//是交点
if (result[count][0] == Picture[j%picture_num]) {//并且相等
count++;
result.push_back({});
}
break;
}
}
//结束
flage = false;
}
else {
for (int j = index[i].first; j < index[(i + 1) % index_num].first; j++) {
result[count].push_back(Frame[j]);
}
//结束
if (result[count][0] == Frame[index[(i + 1) % index_num].first]) {//并且相等
count++;
result.push_back({});
}
flage = true;
}
}
return result;
}