给定x值的三次贝塞尔曲线的y坐标

11

这个问题与二次贝塞尔曲线:给定X值的Y坐标?非常相似,但这是三次的...

我正在使用getBezier函数来计算贝塞尔曲线的Y坐标。 贝塞尔曲线始终从(0,0)开始,并且始终在(1,1)结束。

我知道X值,所以我尝试将其作为百分比插入(我是个白痴)。 但显然那行不通。 你能提供一个解决方案吗? 必须是傻瓜式的函数。 像这样:

function yFromX (c2x,c2y,c3x,c3y) { //c1 = (0,0) and c4 = (1,1), domainc2 and domainc3 = [0,1]
    //your magic
    return y;
}

1
你需要明白,这些曲线是从“百分比”到(X,Y)的函数。你还可以从中受益,了解在三次贝塞尔曲线上可能会有两个点(X,Y1),(X,Y2),其中Y1!= Y2。 - ellisbben
1
我忘了告诉你c2x、c2y、c3x和c3y的范围是[0,1],所以这是不可能的。 - bopjesvla
@bpjesvla:就数学而言,这并不是不可能的。 - Brian Vandenberg
那是@elisbben。不,这不是作业,我正在尝试在一个htc文件中让IE工作过渡效果。这是我迄今遇到的唯一问题。我不是数学学生... - bopjesvla
@bopjesvla 明白了。这意味着该函数是单调的,这使得问题变得非常简单... - ellisbben
4个回答

9

由于问题非常有限(函数x(t)是单调的),我们可能可以使用一种相当便宜的解决方法-二分查找。

var bezier = function(x0, y0, x1, y1, x2, y2, x3, y3, t) {
    /* whatever you're using to calculate points on the curve */
    return undefined; //I'll assume this returns array [x, y].
};

//we actually need a target x value to go with the middle control
//points, don't we? ;)
var yFromX = function(xTarget, x1, y1, x2, y2) {
  var xTolerance = 0.0001; //adjust as you please
  var myBezier = function(t) {
    return bezier(0, 0, x1, y1, x2, y2, 1, 1, t);
  };

  //we could do something less stupid, but since the x is monotonic
  //increasing given the problem constraints, we'll do a binary search.

  //establish bounds
  var lower = 0;
  var upper = 1;
  var percent = (upper + lower) / 2;

  //get initial x
  var x = myBezier(percent)[0];

  //loop until completion
  while(Math.abs(xTarget - x) > xTolerance) {
    if(xTarget > x) 
      lower = percent;
    else 
      upper = percent;

    percent = (upper + lower) / 2;
    x = myBezier(percent)[0];
  }
  //we're within tolerance of the desired x value.
  //return the y value.
  return myBezier(percent)[1];
};

这肯定会在一些超出您约束条件的输入上出现问题。


即使我的知識有限,我應該也能找到這個...謝謝! - bopjesvla
1
考虑到中间两个控制点的约束条件,您对 y(x) 是单值的直觉可能有所限制,但您非常敏捷。请随意点赞或接受我的答案 ;) - ellisbben

9

我使用了来自这个页面的算法,并将其用 JavaScript 编写下来。到目前为止,我测试的所有情况都可以使用它(而且不使用 while 循环)。

调用 solveCubicBezier 函数,传入所有控制点的 x 值和你想要获取 y 坐标值的 x 值。例如:

var results = solveCubicBezier(p0.x, p1.x, p2.x, p3.x, myX);

results是一个数组,包含最初传递给Bezier函数的“t”值。该数组可以包含0到3个元素,因为并非所有x值都有相应的y值,有些甚至有多个。

function solveQuadraticEquation(a, b, c) {

    var discriminant = b * b - 4 * a * c;

    if (discriminant < 0) {
        return [];

    } else {
        return [
            (-b + Math.sqrt(discriminant)) / (2 * a),
            (-b - Math.sqrt(discriminant)) / (2 * a)
        ];
    }

}

function solveCubicEquation(a, b, c, d) {

    if (!a) return solveQuadraticEquation(b, c, d);

    b /= a;
    c /= a;
    d /= a;

    var p = (3 * c - b * b) / 3;
    var q = (2 * b * b * b - 9 * b * c + 27 * d) / 27;

    if (p === 0) {
        return [ Math.pow(-q, 1 / 3) ];

    } else if (q === 0) {
        return [Math.sqrt(-p), -Math.sqrt(-p)];

    } else {

        var discriminant = Math.pow(q / 2, 2) + Math.pow(p / 3, 3);

        if (discriminant === 0) {
            return [Math.pow(q / 2, 1 / 3) - b / 3];

        } else if (discriminant > 0) {
            return [Math.pow(-(q / 2) + Math.sqrt(discriminant), 1 / 3) - Math.pow((q / 2) + Math.sqrt(discriminant), 1 / 3) - b / 3];

        } else {

            var r = Math.sqrt( Math.pow(-(p/3), 3) );
            var phi = Math.acos(-(q / (2 * Math.sqrt(Math.pow(-(p / 3), 3)))));

            var s = 2 * Math.pow(r, 1/3);

            return [
                s * Math.cos(phi / 3) - b / 3,
                s * Math.cos((phi + 2 * Math.PI) / 3) - b / 3,
                s * Math.cos((phi + 4 * Math.PI) / 3) - b / 3
            ];

        }

    }
}

function roundToDecimal(num, dec) {
    return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
}

function solveCubicBezier(p0, p1, p2, p3, x) {

    p0 -= x;
    p1 -= x;
    p2 -= x;
    p3 -= x;

    var a = p3 - 3 * p2 + 3 * p1 - p0;
    var b = 3 * p2 - 6 * p1 + 3 * p0;
    var c = 3 * p1 - 3 * p0;
    var d = p0;

    var roots = solveCubicEquation(
        p3 - 3 * p2 + 3 * p1 - p0,
        3 * p2 - 6 * p1 + 3 * p0,
        3 * p1 - 3 * p0,
        p0
    );

    var result = [];
    var root;
    for (var i = 0; i < roots.length; i++) {
        root = roundToDecimal(roots[i], 15);
        if (root >= 0 && root <= 1) result.push(root);
    }

    return result;

}

1
嗨,我在使用以下数值时遇到了错误:solveCubicBezier(13.969161963240431, 74.2679238696713, 303.900002823836, 901.3362308681752, 300)问题似乎是由方程式Math.pow((q / 2) + Math.sqrt(discriminant), 1 / 3)返回Nan引起的。它试图计算将负数提高到0.3次幂的结果,这是不可能的。实际上,它正在尝试计算Math.Pow(-0.08, 0.3)。有什么想法吗? - user346443
4
立方根函数可以这样实现:function crt(v) { if(v<0) return -Math.pow(-v,1/3); return Math.pow(v,1/3); }。这只会给出实部,而不是包括虚数根在内的所有三个根,因为我们通常不能在JS中使用复数。我创建了一个包含该立方根函数的实现,网址是http://jsbin.com/fewih/1/edit。 - Mike 'Pomax' Kamermans
1
为什么要定义abcd,然后在调用solveCubicEquation时不使用这些值呢? - Labrador

1

我已经在Java中实现了这个解决方案,运行得非常好。但最重要的是我理解它。谢谢!

public class CubicBezier {
    private BezierCubic bezier = new BezierCubic();
    public CubicBezier(float x1, float y1, float x2, float y2) {
        bezier.set(new Vector3(0,0,0), new Vector3(x1,y1,0), new Vector3(x2,y2,0), new Vector3(1,1,1));
    }
    public float get(float t) {
        float l=0, u=1, s=(u+l)*0.5f;
        float x = bezier.getValueX(s);
        while (Math.abs(t-x) > 0.0001f) {
            if (t > x)  { l = s; }
            else        { u = s; }
            s = (u+l)*0.5f;
            x = bezier.getValueX(s);
        }
        return bezier.getValueY(s);
    }
};

public class BezierCubic {
private float[][] cpoints = new float[4][3];
private float[][] polinom = new float[4][3];

public BezierCubic() {}

public void set(Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3) {
    setPoint(0, c0);
    setPoint(1, c1);
    setPoint(2, c2);
    setPoint(3, c3);
    generate();
}

public float getValueX(float u) {
    return getValue(0, u);
}

public float getValueY(float u) {
    return getValue(1, u);
}

public float getValueZ(float u) {
    return getValue(2, u);
}

private float getValue(int i, float u) {
    return ((polinom[0][i]*u + polinom[1][i])*u + polinom[2][i])*u + polinom[3][i];
}

private void generate() {
    for (int i=0; i<3; i++) {
        float c0 = cpoints[0][i], c1 = cpoints[1][i], c2 = cpoints[2][i], c3 = cpoints[3][i];
        polinom[0][i] = c0 + 3*(c1 - c2) + c3;
        polinom[1][i] = 3*(c0 - 2*c1 + c2);
        polinom[2][i] = 3*(-c0 + c1);
        polinom[3][i] = c0;
    }
}

private void setPoint(int i, Vector3 v) {
    cpoints[i][0] = v.x;
    cpoints[i][1] = v.y;
    cpoints[i][2] = v.z;
}

} }


0

原始答案已经包含了你需要知道的一切

存在数字问题。三次方程的精确解决方案存在稳定性问题。

典型贝塞尔曲线的平滑几何特性意味着空间搜索(递归细分)收敛良好,通常“足够快”,并且可以轻松扩展到N维。在Qt源代码中有一个相当易读的实现。


对于我来说,这个不够傻瓜化 :) 如果可以的话,你能把它写出来吗? - bopjesvla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接