对于个人项目,我需要找出两条立方Bézier曲线是否相交。我不需要知道在哪里:我只需要知道他们是否这样做。但是,我需要快速完成。
我一直在清理这个地方,我找到了几个资源。大多数情况下,this question here有一个很有希望的答案。
所以在我确定什么是Sylvester matrix之后,什么是determinant,什么是resultant和why it's useful,我想我认为解决方案是如何工作的。但是,现实不同,而且效果不好。
我用我的图形计算器绘制了两个相交的Bézier样条(我们称之为B 0 和B 1 )。它们的坐标如下(P 0 ,P 1 ,P 2 ,P 3 ):
(1, 1) (2, 4) (3, 4) (4, 3)
(3, 5) (3, 6) (0, 1) (3, 1)
结果如下,B 0 是“水平”曲线而B 1 是另一个:
按照上述问题的最高投票答案的指示,我将B 0 减去B 1 。根据我的计算器,它给我留下了两个方程式(X轴和Y轴):
x = 9t^3 - 9t^2 - 3t + 2
y = 9t^3 - 9t^2 - 6t + 4
由此我构建了以下西尔维斯特矩阵:
9 -9 -3 2 0 0
0 9 -9 -3 2 0
0 0 9 -9 -3 2
9 -9 -6 4 0 0
0 9 -9 -6 4 0
0 0 9 -9 -6 4
之后,我使用Laplace expansion制作了一个C ++函数来计算矩阵的行列式:
template<int size>
float determinant(float* matrix)
{
float total = 0;
float sign = 1;
float temporaryMatrix[(size - 1) * (size - 1)];
for (int i = 0; i < size; i++)
{
if (matrix[i] != 0)
{
for (int j = 1; j < size; j++)
{
float* targetOffset = temporaryMatrix + (j - 1) * (size - 1);
float* sourceOffset = matrix + j * size;
int firstCopySize = i * sizeof *matrix;
int secondCopySize = (size - i - 1) * sizeof *matrix;
memcpy(targetOffset, sourceOffset, firstCopySize);
memcpy(targetOffset + i, sourceOffset + i + 1, secondCopySize);
}
float subdeterminant = determinant<size - 1>(temporaryMatrix);
total += matrix[i] * subdeterminant * sign;
}
sign *= -1;
}
return total;
}
template<>
float determinant<1>(float* matrix)
{
return matrix[0];
}
它似乎在相对较小的矩阵(2x2,3x3和4x4)上工作得很好,所以我希望它也适用于6x6矩阵。然而,我并没有进行大量的测试,而且它有可能被打破。
如果我从另一个问题中正确理解了答案,那么因为曲线相交,行列式应该是0。但是,为我的程序提供上面提到的西尔维斯特矩阵,它是-2916。
我的结局或结局都是错误的吗?找出两条立方贝塞尔曲线是否相交的正确方法是什么?
答案 0 :(得分:14)
Bezier曲线的交点由(非常酷)Asymptote矢量图形语言完成:寻找intersect()
here。
虽然他们没有解释他们实际使用的算法,除了说它来自p。 “The Metafont Book”的第137页,它的关键似乎是Bezier曲线的两个重要属性(虽然我现在找不到该页面,但在该网站的其他地方已有解释):
使用这两个属性和交叉多边形的算法,您可以递归到任意精度:
如果曲线不相交,这将是快速的 - 这是通常情况吗?
[编辑] 看起来将贝塞尔曲线分成两部分的算法称为de Casteljau's algorithm。
答案 1 :(得分:7)
如果您正在为生产代码执行此操作,我建议使用Bezier剪切算法。它在section 7.7 of this free online CAGD text(pdf)中得到了很好的解释,适用于任何程度的贝塞尔曲线,并且快速而稳健。
虽然从数学角度来看使用标准的根执行器或矩阵可能更直接,但Bezier裁剪相对容易实现和调试,并且实际上具有较少的浮点误差。这是因为每当它创建新数字时,它都在进行加权平均(凸组合),因此没有机会根据噪声输入进行外推。
答案 2 :(得分:3)
我的结局或结局是错误的吗?
您是否基于this answer附带的第4条评论对决定因素进行了解释?如果是这样,我相信这就是错误所在。在此复制评论:
如果行列式为零则存在 X中的根和*完全相同 t的值,所以有一个 两条曲线的交点。 (t 可能不在0..1的区间内 虽然)。
我没有看到这部分有任何问题,但作者继续说:
如果行列式是&lt;&gt;零,你可以 确保曲线没有 在任何地方相交。
我不认为这是对的。两条曲线完全可能在t值不同的位置相交,在这种情况下,即使矩阵具有非零行列式,也会有一个交点。我相信这就是你的情况。
答案 3 :(得分:2)
我绝不是这方面的专家,但我跟随了一个很好的关于曲线的blog。他链接到两篇关于你的问题的好文章(第二个链接有一个交互式演示和一些源代码)。其他人可能对这个问题有更好的了解,但我希望这会有所帮助!
答案 4 :(得分:2)
我不知道它会有多快,但如果你有两条曲线C1(t)和C2(k),它们会相交,如果C1(t)== C2(k)。因此,对于两个变量(t,k),您有两个方程(每x和每y)。您可以使用数值方法解决系统,足以保证您的准确性。当你找到t,k参数时,你应该检查[0,1]上是否有参数。如果它们在[0,1]上相交。
答案 5 :(得分:2)
这是一个难题。我会将2条贝塞尔曲线中的每条曲线分成5-10个离散线段,然后进行直线交叉。
foreach SampledLineSegment line1 in Bezier1
foreach SampledLineSegment line2 in Bezier2
if( line1 intersects line2 )
then Bezier1 intersects Bezier2
答案 6 :(得分:0)
我想说最简单也可能最快的答案是将它们细分为非常小的线条并找到曲线相交的点,如果实际的话。
public static void towardsCubic(double[] xy, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, double t) {
double x, y;
x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3;
y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3;
xy[0] = x;
xy[1] = y;
}
public static void towardsQuad(double[] xy, double x0, double y0, double x1, double y1, double x2, double y2, double t) {
double x, y;
x = (1 - t) * (1 - t) * x0 + 2 * (1 - t) * t * x1 + t * t * x2;
y = (1 - t) * (1 - t) * y0 + 2 * (1 - t) * t * y1 + t * t * y2;
xy[0] = x;
xy[1] = y;
}
显然蛮力的回答很糟糕请参阅bo {4}的回答,并且有很多线性几何和碰撞检测实际上会有很多帮助。
选择曲线所需的切片数量。像100这样的东西应该很棒。
我们采用所有细分,并根据他们拥有的最大值对它们进行排序。然后,我们在列表中添加第二个标志,用于该段的最小y值。
我们保留一个活动边缘列表。
我们遍历y排序的段列表,当遇到一个前导段时,我们将它添加到活动列表中。当我们点击小y标记的值时,我们会从活动列表中删除该段。
然后我们可以简单地遍历整个区段集,其数量相当于扫描线,在我们简单地迭代列表时单调地增加y。我们遍历排序列表中的值,这通常会删除一个段并添加一个新段(或者对于拆分和合并节点,添加两个段或删除两个段)。从而保持相关细分的活动列表。
我们运行快速失败交叉检查,因为我们将新的活动段添加到活动段列表中,仅针对该段和当前活动的段。
因此,在我们遍历曲线的采样段时,我们始终确切地知道哪些线段是相关的。我们知道这些段在y-coords中有重叠。我们可以快速地使任何与其x-coords不重叠的新段快速失败。在罕见的情况下,它们在x-coords中重叠,然后我们检查这些段是否相交。
这可能会将线路交叉点检查的数量减少到基本上相交的数量。
foreach(segment in sortedSegmentList) {
if (segment.isLeading()) {
checkAgainstActives(segment);
actives.add(segment);
}
else actives.remove(segment)
}
checkAgainstActive()只会检查此段和活动列表中的任何段是否与x-coords重叠,如果有,则对它们执行行相交检查,并采取相应的操作。
另请注意,这适用于任何数量的曲线,任何类型的曲线,任何混合曲线的任何顺序。如果我们遍历整个段的列表,它将找到每个交叉点。它将找到Bezier与圆相交的每个点或十几个二次Bezier曲线彼此相交的每个交点(或它们自己),并且都在相同的瞬间。
对于细分算法,经常引用Chapter 7 document注释:
“一旦一对曲线被细分到足以使它们各自被线段近似到公差范围内”
我们可以简单地跳过中间人。我们可以足够快地执行此操作,以便简单地比较线段与曲线中可容忍的误差量。最后,这就是典型答案所做的。
其次,请注意,此处碰撞检测的大部分速度增加,即根据其最高y排序的有序列表,以添加到活动列表,以及从活动列表中删除的最低y,同样可以直接对贝塞尔曲线的船体多边形进行。我们的线段只是一个2阶的多边形,但我们可以像平凡一样做任意数量的点,并按照我们正在处理的任何曲线顺序检查所有控制点的边界框。因此,我们将有一个活动的船体多边形点列表,而不是活动段的列表。我们可以简单地使用De Casteljau的算法来分割曲线,从而将其作为细分曲线而不是线段进行采样。因此,不是2分我们有4或7或其他什么,并且运行相同的例程非常适合快速失败。
在最大y处添加相关的点组,在最小时删除它,并仅比较活动列表。因此,我们可以快速,更好地实现Bezier细分算法。通过简单地找到边界框重叠,然后细分那些重叠的曲线,并删除那些没有重叠的曲线。同样,我们可以做任意数量的曲线,甚至是从前一次迭代中的曲线细分的曲线。就像我们的分段近似可以非常快速地快速求解数百条不同曲线(甚至是不同阶数)之间的每个交叉点位置。只需检查所有曲线以查看边界框是否重叠,如果它们重叠,我们将它们细分,直到我们的曲线足够小或我们用完它们。
答案 7 :(得分:0)
是的,我知道,这个帖子已被接受并关闭了很长时间,但是......
首先,我要感谢你, zneak ,寻求灵感。你的努力可以找到正确的方法。
其次,我对接受的解决方案不太满意。这种用于我最喜欢的免费软件IPE,它的bugzilla有很多抱怨因为交叉点问题的准确性和可靠性低,我的其中之一。
允许以 zneak 提出的方式解决问题的缺失技巧:足以将一条曲线缩短 k &gt; 0 ,则Sylvester矩阵的行列式将等于零。很明显,如果缩短的曲线相交,那么原始曲线也会相交。现在,任务将更改为搜索 k 因子的合适值。这导致解决9度的单变量多项式的问题。该多项式的实根和正根负责势交点。 (这不应该是一个惊喜,两个三次贝塞尔曲线最多可以相交9次。)执行最终选择以仅查找 k 因子,它们提供0&lt;两条曲线的; = t &lt; = 1。
现在是Maxima代码,其中起点由 zneak 提供的8点设置:
p0x:1; p0y:1;
p1x:2; p1y:4;
p2x:3; p2y:4;
p3x:4; p3y:3;
q0x:3; q0y:5;
q1x:3; q1y:6;
q2x:0; q2y:1;
q3x:3; q3y:1;
c0x:p0x;
c0y:p0y;
c1x:3*(p1x-p0x);
c1y:3*(p1y-p0y);
c2x:3*(p2x+p0x)-6*p1x;
c2y:3*(p2y+p0y)-6*p1y;
c3x:3*(p1x-p2x)+p3x-p0x;
c3y:3*(p1y-p2y)+p3y-p0y;
d0x:q0x;
d0y:q0y;
d1x:3*(q1x-q0x);
d1y:3*(q1y-q0y);
d2x:3*(q2x+q0x)-6*q1x;
d2y:3*(q2y+q0y)-6*q1y;
d3x:3*(q1x-q2x)+q3x-q0x;
d3y:3*(q1y-q2y)+q3y-q0y;
x:c0x-d0x + (c1x-d1x*k)*t+ (c2x-d2x*k^2)*t^2+ (c3x-d3x*k^3)*t^3;
y:c0y-d0y + (c1y-d1y*k)*t+ (c2y-d2y*k^2)*t^2+ (c3y-d3y*k^3)*t^3;
z:resultant(x,y,t);
此时,千里马回答:
(%o35)−2*(1004*k^9−5049*k^8+5940*k^7−1689*k^6+10584*k^5−8235*k^4−2307*k^3+1026*k^2+108*k+76)
让Maxima解决这个等式:
rr: float( realroots(z,1e-20))
答案是:
(%o36) [k=−0.40256438624399,k=0.43261490325108,k=0.84718739982868,k=2.643321910825066,k=2.71772491293651]
现在选择正确值 k 的代码:
for item in rr do (
evk:ev(k,item),
if evk>0 then (
/*print("k=",evk),*/
xx:ev(x,item), rx:float( realroots(xx,1e-20)),/*print("x(t)=",xx," roots: ",rx),*/
yy:ev(y,item), ry:float( realroots(yy,1e-20)),/*print("y(t)=",yy," roots: ",ry),*/
for it1 in rx do ( t1:ev(t,it1),
for it2 in ry do ( t2:ev(t,it2),
dt:abs(t1-t2),
if dt<1e-10 then (
/*print("Common root=",t1," delta t=",dt),*/
if (t1>0) and (t1<=1) then ( t2:t1*evk,
if (t2>0) and (t2<=1) then (
x1:c0x + c1x*t1+ c2x*t1^2+ c3x*t1^3,
y1:c0y + c1y*t1+ c2y*t1^2+ c3y*t1^3,
print("Intersection point: x=",x1, " y=",y1)
)))))/*,disp ("-----")*/
));
Maxima的回答:
"Intersection point: x="1.693201254437358" y="2.62375005067273
(%o37) done
不过,Theres不仅是一种亲爱的。如果出现以下情况,则所提出的方法无法使用:
有人可以问,为什么缩短只进行一次。这已经足够了,因为反向反演法被发现了 en passant ,但这是另一个故事。