多边形裁剪 - 一点点详细说明

Polygon clipping - a little elaboration?

本文关键字:说明 一点点 多边形裁剪      更新时间:2023-10-16

我一直在阅读很多关于萨瑟兰霍奇曼多边形裁剪算法的信息,并了解了大致的想法。但是,当我看到它的实际实现(如下图)时,我对坐标比较感到困惑,例如intersectioninside方法中的坐标比较。因此,我想知道是否有人可以详细说明什么以及为什么?我看到大量解释一般概念的视频和文章,但我真的很难找到有关实现的实际细节的一些解释。

bool inside(b2Vec2 cp1, b2Vec2 cp2, b2Vec2 p) {
return (cp2.x-cp1.x)*(p.y-cp1.y) > (cp2.y-cp1.y)*(p.x-cp1.x);
}
b2Vec2 intersection(b2Vec2 cp1, b2Vec2 cp2, b2Vec2 s, b2Vec2 e) {
b2Vec2 dc( cp1.x - cp2.x, cp1.y - cp2.y );
b2Vec2 dp( s.x - e.x, s.y - e.y );
float n1 = cp1.x * cp2.y - cp1.y * cp2.x;
float n2 = s.x * e.y - s.y * e.x;
float n3 = 1.0 / (dc.x * dp.y - dc.y * dp.x);
return b2Vec2( (n1*dp.x - n2*dc.x) * n3, (n1*dp.y - n2*dc.y) * n3);
}
//http://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#JavaScript
//Note that this only works when fB is a convex polygon, but we know all 
//fixtures in Box2D are convex, so that will not be a problem
bool findIntersectionOfFixtures(b2Fixture* fA, b2Fixture* fB, vector<b2Vec2>& outputVertices)
{
//currently this only handles polygon vs polygon
if ( fA->GetShape()->GetType() != b2Shape::e_polygon ||
fB->GetShape()->GetType() != b2Shape::e_polygon )
return false;
b2PolygonShape* polyA = (b2PolygonShape*)fA->GetShape();
b2PolygonShape* polyB = (b2PolygonShape*)fB->GetShape();
//fill subject polygon from fixtureA polygon
for (int i = 0; i < polyA->GetVertexCount(); i++)
outputVertices.push_back( fA->GetBody()->GetWorldPoint( polyA->GetVertex(i) ) );
//fill clip polygon from fixtureB polygon
vector<b2Vec2> clipPolygon;
for (int i = 0; i < polyB->GetVertexCount(); i++)
clipPolygon.push_back( fB->GetBody()->GetWorldPoint( polyB->GetVertex(i) ) );
b2Vec2 cp1 = clipPolygon[clipPolygon.size()-1];
for (int j = 0; j < clipPolygon.size(); j++) {
b2Vec2 cp2 = clipPolygon[j];
if ( outputVertices.empty() )
return false;
vector<b2Vec2> inputList = outputVertices;
outputVertices.clear();
b2Vec2 s = inputList[inputList.size() - 1]; //last on the input list
for (int i = 0; i < inputList.size(); i++) {
b2Vec2 e = inputList[i];
if (inside(cp1, cp2, e)) {
if (!inside(cp1, cp2, s)) {
outputVertices.push_back( intersection(cp1, cp2, s, e) );
}
outputVertices.push_back(e);
}
else if (inside(cp1, cp2, s)) {
outputVertices.push_back( intersection(cp1, cp2, s, e) );
}
s = e;
}
cp1 = cp2;
}
return !outputVertices.empty();
}

(代码从iForce2d :)窃取)

你说你理解了这个大致的想法,大概是通过阅读萨瑟兰霍奇曼算法之类的东西。 这在高层次上解释了insideintersection做什么。

至于他们如何实现目标的细节,那都只是直接的教科书线性代数。

inside正在测试交叉(p - cp1)(cp2 - cp2)的符号,并在符号严格大于零true返回。 您可以将 return 语句重写为:

return (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x) > 0;

通过将第二项移动到>的左侧,这将为您提供左侧的叉积。

请注意,叉积通常是vec3的叉vec3运算,需要计算所有三个项。 但是,我们在 2d 中执行此操作,这意味着vec3具有(x, y, 0)的形式。 因此,我们只需要计算z输出项,因为十字必须垂直于xy平面,因此形式为(0, 0, value)

intersection精确地使用此处列出的算法找到两个向量相交的点:从两点开始的线相交。 特别是,我们关心紧跟在文本"行列式可以写成:"之后的公式:

在该公式的上下文中,n1(x1 y2 - y1 x2)n2(x3 y4 - y3 x4)的,n31 / ((x1 - x2) (y3 - y4) - (y1 - y2) (x3 - x4))

--编辑--

为了涵盖评论中提出的问题,这里有一个尽可能完整的解释,说明为什么inside()的返回值是对交叉积符号的测试。

我将稍微偏离一点切线,显示我的年龄,并注意交叉乘积公式有一个非常简单的记忆辅助。 你只需要记住伍兹和克劳瑟的文字冒险游戏《巨大洞穴》中的第一个魔法词。xyzzy.

如果你在三维中有两个向量:(x1, y1, z1)(x2, y2, z2),它们的交叉乘积(xc, yc, zc)计算如下:

xc = y1 * z2 - z1 * y2;
yc = z1 * x2 - x1 * z2;
zc = x1 * y2 - y1 * x2;

现在,查看第一行,从术语、所有空格和运算符中删除c12后缀,只查看剩余的字母。 这是一个神奇的词。 然后你只需垂直向下,用y替换xyz替换,在你从一条线到另一行时用xz

言归正传,xcyc扩展右侧的术语都包含z1z2。 但我们知道这两者都为零,因为我们的输入向量在xy平面上,因此具有零z分量。 这就是为什么我们可以完全省略计算这两个项,因为我们知道它们将为零。

这与叉积的定义 100% 一致,生成的向量始终垂直于两个输入向量。 因此,如果两个输入向量都在xy平面上,我们知道输出向量必须垂直于xy平面,因此具有(0, 0, z)

那么,我们对z任期有什么看法呢?

zc = x1 * y2 - y1 * x2;

在这种情况下,向量1cp2-cp1,向量2p-cp1。 因此,将其插入上述内容,我们得到:

zc = (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x);

但如前所述,我们不关心它的价值,只关心它的标志。 我们想知道这是否大于零。 因此:

return (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x) > 0;

然后将其重写为:

return (cp2.x-cp1.x)*(p.y-cp1.y) > (cp2.y-cp1.y)*(p.x-cp1.x);

最后,该项的符号与点 p 是在裁剪多边形内部还是外部有什么关系? 您说得很对,所有的剪辑都发生在 2Dxy平面上,那么为什么我们要涉及 3D 操作呢?

重要的是要认识到 3D 中的交叉乘积公式不是可交换的。 就两个向量操作数之间的角度而言,它们之间的顺序很重要。 维基百科页面上的Cross产品第一张图片完美地展示了它。 在该图中,如果您从上方向下看,在评估a交叉b时,从ab的最短角度方向是逆时针方向。 在这种情况下,这会导致具有正z值的交叉乘积,假设正z上升到页面上。 但是,如果您评估b交叉a,则从ba的最浅角距离是顺时针方向,并且交叉积具有负z值。

回想一下算法本身的维基百科页面,你已经得到了一条蓝色的"裁剪"线,它围绕裁剪多边形逆时针工作。 如果您认为该矢量在逆时针方向上始终具有正量级,则对于裁剪多边形中的任何一对相邻折点,它将始终cp2 - cp1

记住这一点,想象一下,如果你站在cp1,鼻子直指cp2,你会看到什么。 裁剪多边形的内部将位于左侧,外部位于右侧。 现在考虑两点p1p2。 我们会说p1在剪切多边形内部,p2在外部。 这意味着将鼻子指向p1的最快方法是逆时针旋转,而指向p2的最快方法是顺时针旋转。

因此,通过研究叉积的符号,我们实际上是在问"我们是从当前边缘顺时针旋转还是逆时针旋转以查看点",这相当于询问该点是在裁剪多边形内部还是外部。

我将补充最后一个建议。 如果你对这类东西感兴趣,或者3D渲染,或任何涉及对现实世界的数学表示进行建模的编程,那么参加一门很好的线性代数课程,涵盖交叉积,点积,向量,矩阵以及它们之间的相互作用,将是你能做的最好的事情之一。 它将为计算机完成的大量工作提供非常坚实的基础。