首先,这个答案只适用于你的控制点约束意味着我们总是处理一个正常函数的参数等价形式。在这种情况下,这显然是你想要的,但是任何在未来发现这个答案的人都应该知道,这个答案基于这样一个
假设:对于任何给定的x值,只有一个y值...
这绝对不适用于一般的贝塞尔曲线
话虽如此,我们知道,尽管我们已经将这条曲线表示为二维参数曲线,但我们正在处理的曲线必须具有某种未知形式的函数y = f(x)
。我们还知道,只要我们知道唯一属于特定x的“t”值(这仅在严格单调递增的系数属性的情况下才成立),我们就可以计算y作为y = By(t)
,因此问题是:我们能否计算我们需要插入By(t)
的t
值,给定一些已知的x
值?
答案是:是的,我们可以。
首先,我们开始的任何x值都有自己的x = Bx(t),因此,鉴于我们知道x,我们应该能够解决该函数并找到导致该x的相应的t值。
让我们看一下x(t)的函数:
x(t) = a(1-t)³ + 3b(1-t)²t + 3c(1-t)t² + dt³
我们可以将其重写为简单的多项式形式:
x(t) = (-a + 3b- 3c + d)t³ + (3a - 6b + 3c)t² + (-3a + 3b)t + a
这是一个标准的三次多项式,只使用已知常数作为系数,我们可以轻松地将其重写为:
x = (-a + 3b- 3c + d)t³ + (3a - 6b + 3c)t² + (-3a + 3b)t + a
0 = (-a + 3b- 3c + d)t³ + (3a - 6b + 3c)t² + (-3a + 3b)t + (a-x)
现在我们只需要解决这个方程:我们知道除了t
之外的每一个值,我们只需要一些数学见解来告诉我们如何做。
当然,"只是"不是正确的限定词,在找到三次函数的根方面没有什么"简单"的,但幸运的是,16世纪Gerolano Cardano利用复数奠定了确定根的基础。在任何人发明复数之前。相当了不起!但这是一篇编程答案,不是历史课,所以让我们开始实施:
给定一些已知的x值,和对于我们的坐标a、b、c和d的知识,我们可以按照以下方式实现我们的根查找:
double[] findRoots(double x, double[] coordinates) {
double
pa = coordinates[0],
pb = coordinates[1],
pc = coordinates[2],
pd = coordinates[3],
pa3 = 3 * pa,
pb3 = 3 * pb,
pc3 = 3 * pc,
a = -pa + pb3 - pc3 + pd,
b = pa3 - 2*pb3 + pc3,
c = -pa3 + pb3,
d = pa - x;
if (approximately(a, 0)) {
if (approximately(b, 0)) {
if (approximately(c, 0)) {
return new double[]{};
}
return new double[]{-d / c};
}
double
q = sqrt(c * c - 4 * b * d),
b2 = 2 * b;
return new double[]{
(q - c) / b2,
(-c - q) / b2
};
}
b /= a;
c /= a;
d /= a;
double
b3 = b / 3,
p = (3 * c - b*b) / 3,
p3 = p / 3,
q = (2 * b*b*b - 9 * b * c + 27 * d) / 27,
q2 = q / 2,
discriminant = q2*q2 + p3*p3*p3,
u1, v1;
if (discriminant < 0) {
double
mp3 = -p/3,
r = sqrt(mp3*mp3*mp3),
t = -q / (2 * r),
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
phi = acos(cosphi),
crtr = crt(r),
t1 = 2 * crtr;
return new double[]{
t1 * cos(phi / 3) - b3,
t1 * cos((phi + TAU) / 3) - b3,
t1 * cos((phi + 2 * TAU) / 3) - b3
};
}
else if (discriminant == 0) {
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
return new double[]{
2 * u1 - b3,
-u1 - b3
};
}
else {
double sd = sqrt(discriminant);
u1 = crt(-q2 + sd);
v1 = crt(q2 + sd);
return new double[]{u1 - v1 - b3};
}
}
好的,这是一大块代码,有一些额外的内容:
crt()
是立方根函数。在本例中,我们实际上不关心复数,所以更容易的方式是使用类的定义、宏、三元运算符或你所选择语言的任何缩写:crt(x) = x < 0 ? -pow(-x, 1f/3f) : pow(x, 1f/3f);
。
tau
就是2π。当你进行几何编程时,它很有用。
approximately
是一个将值与目标周围的非常小区间进行比较的函数,因为 IEEE 浮点数是混蛋。基本上,我们说 approximately(a,b) = return abs(a-b) < 0.000001
或者其他什么。
其余部分应该相当容易理解,如果有点 Java 式(我在使用Processing进行这些操作)。
通过这个实现,我们可以编写我们的实现来找到给定x的y。现在,数学可能会产生多个根,但是我们
非常特定的初始条件意味着我们知道在用于Bezier曲线的[0,1]区间中只有一个根,因此我们可以简单地检查哪个值符合该标准,然后使用该根计算y(t)。
double xCoordinates = [...];
double yCoordinates = [...];
...
double x = some value we know!
double[] roots = findRoots(x, xCoordinates);
double t;
if (roots.length > 0) {
for (double r: roots) {
if (r < 0 || r > 1) continue;
t = r;
break; }}
double y = compute(t, yCoordinates);
就这样,我们完成了:现在我们有了“t”值,可以用它来获取相应的“y”值。
t
的负值。 这是我使用的代码(我根据图形命名了变量。它是C#,Vector
有2个double
组件)。您发现任何错误吗? - Jakecubic-bezier(0.4, 0.0, 0.2, 1)
: 该曲线由(0, 0), (0.4, 0), (0.2, 1), (1, 1)
定义。 因此,为了使用findRoots
,请像这样调用它:findRoots(x, 0f, controlA.x, controlB.x, 1f)
。 将结果根据答案中的roots.length
检查运行。我不得不修改它以删除/添加浮动容差0.00001。 这会给你对应于你的x
的t
。 通过多项式贝塞尔方程运行t
以获得y
:y = controlA.y * 3 * (1 - t) * (1 - t) * t + controlB.y * 3 * (1 - t) * t * t + t * t * t
。 - Alexander Dorokhine