如何在贝塞尔曲线上实现均匀的移动速度?


22

我正在尝试沿贝塞尔曲线移动图像。这是我的方法:

- (void)startFly
{    
 [self runAction:[CCSequence actions:
             [CCBezierBy actionWithDuration:timeFlying bezier:[self getPathWithDirection:currentDirection]],
             [CCCallFuncN actionWithTarget:self selector:@selector(endFly)],
             nil]];

}

我的问题是图像移动不均匀。刚开始时它运行缓慢,然后逐渐加速,最后它运行得非常快。我应该怎么做才能摆脱这种加速?

Answers:


27

对于大多数参数轨迹,可以近似解决该问题。想法如下:如果在曲线上放大足够深,则无法从该点的切线知道曲线本身。

通过进行此假设,无需预先计算两个以上的向量(三次贝塞尔曲线三个)。

因此,对于曲线中号Ť我们计算其切线向量d中号dŤŤd中号dŤΔŤd中号dŤΔŤ大号大号÷d中号dŤ

应用:二次贝塞尔曲线

如果贝塞尔曲线的控制点是,和,则轨迹可以表示为:一种C

中号Ť=1个-Ť2一种+2Ť1个-Ť+Ť2C=Ť2一种-2+C+Ť-2一种+2+一种

因此,导数为:

d中号dŤ=Ť2一种-4+2C+-2一种+2

您只需要将向量和某个位置。然后,对于给定的,如果要前进长度,则可以执行以下操作:v1个=2一种-4+2Cv2=-2一种+2Ť大号

t=t+Llength(tv1+v2)

三次贝塞尔曲线

相同的推理适用于具有四个控制点,,和的曲线:ABCD

M(t)=(1t)3A+3t(1t)2B+3t2(1t)C+Ť3d=Ť3-一种+3-3C+d+Ť23一种-6+3C+Ť-3一种+3+一种

导数是:

dMdt=t2(3A+9B9C+3D)+t(6A12B+6C)+(3A+3B)

我们预先计算了三个向量:

v1=3A+9B9C+3Dv2=6A12B+6Cv3=3A+3B

最终公式为:

t=t+Llength(t2v1+tv2+v3)

准确性问题

如果您以合理的帧速率运行,则(应根据帧持续时间进行计算)将足够小,以使其近似工作。L

但是,在极端情况下,您可能会遇到错误。如果太大,则可以分段计算,例如使用10个部分:大号

for (int i = 0; i < 10; i++)
    t = t + (L / 10) / length(t * v1 + v2);

1
你好 我正在阅读您的答案,但我不明白L是什么。您所说的“应根据帧持续时间计算”是什么意思?
Michael IV

L =曲线段长度吗?
迈克尔四世

L是曲线长度,即您要在当前帧中移动的距离。
sam hocevar,

好,我现在知道了。您认为从下面的答案中得出的近似值与曲线拆分技术一样好吗?
迈克尔四世

L足够小时,这种近似实际上总是比下面的答案更准确,是的。它还使用较少的内存(因为它使用导数而不是存储所有点值)。当L开始增长时,您可以使用我最后建议的技术。
sam hocevar '18

6

您需要重新设置曲线的参数。最简单的方法是计算曲线几段的弧长,并使用这些长度来确定应从何处采样。例如,也许在t = 0.5(中途通过)处,您应该将s = 0.7传递到曲线上以获得“中途”位置。为此,需要存储各种曲线段的弧长列表。

可能有更好的方法,但这是我在游戏中编写的一些非常简单的C#代码。移植到目标C应该很容易:

public sealed class CurveMap<TCurve> where TCurve : struct, ICurve
{
    private readonly float[] _arcLengths;
    private readonly float _ratio;
    public float length { get; private set; }
    public TCurve curve { get; private set; }
    public bool isSet { get { return !length.isNaN(); } }
    public int resolution { get { return _arcLengths.Length; } }

    public CurveMap(int resolution)
    {
        _arcLengths = new float[resolution];
        _ratio = 1f / resolution;
        length = float.NaN;
    }

    public void set(TCurve c)
    {
        curve = c;
        Vector2 o = c.sample(0);
        float ox = o.X;
        float oy = o.Y;
        float clen = 0;
        int nSamples = _arcLengths.Length;
        for(int i = 0; i < nSamples; i++)
        {
            float t = (i + 1) * _ratio;
            Vector2 p = c.sample(t);
            float dx = ox - p.X;
            float dy = oy - p.Y;
            clen += (dx * dx + dy * dy).sqrt();
            _arcLengths[i] = clen;
            ox = p.X;
            oy = p.Y;
        }
        length = clen;
    }

    public Vector2 sample(float u)
    {
        if(u <= 0) return curve.sample(0);
        if(u >= 1) return curve.sample(1);

        int index = 0;
        int low = 0;
        int high = resolution - 1;
        float target = u * length;
        float found = float.NaN;

        // Binary search to find largest value <= target
        while(low < high)
        {
            index = (low + high) / 2;
            found = _arcLengths[index];
            if (found < target)
                low = index + 1;
            else
                high = index;
        }

        // If the value we found is greater than the target value, retreat
        if (found > target)
            index--;

        if(index < 0) return curve.sample(0);
        if(index >= resolution - 1) return curve.sample(1);

        // Linear interpolation for index
        float min = _arcLengths[index];
        float max = _arcLengths[index + 1];
        Debug.Assert(min <= target && max >= target);
        float interp = (target - min) / (max - min);
        Debug.Assert(interp >= 0 && interp <= 1);
        return curve.sample((index + interp + 1) * _ratio);
    }
}

编辑:值得注意的是,这不会给您确切的弧长,因为不可能获得三次曲线的弧长。所有这一切都是估计各个段的长度。根据曲线的长度,您可能需要提高分辨率,以防止其到达新线段时改变速度。我通常使用〜100,这是我从未遇到过的问题。


0

一种非常轻巧的解决方案是近似速度而不是近似曲线。实际上,这种方法独立于曲线函数,使您可以使用任何精确的曲线,而不必使用导数或近似值。

这是C#Unity 3D的代码:

public float speed; // target linear speed

// determine an initial value by checking where speedFactor converges
float speedFactor = speed / 10; 

float targetStepSize = speed / 60f; // divide by fixedUpdate frame rate
float lastStepSize;

void Update ()
{   
    // Take a note of your previous position.
    Vector3 previousPosition = transform.position;

    // Advance on the curve to the next t;
    transform.position = BezierOrOtherCurveFunction(p0, p1, ..., t);

    // Measure your movement length
    lastStepSize = Vector3.Magnitude(transform.position - previousPosition);

    // Accelerate or decelerate according to your latest step size.
    if (lastStepSize < targetStepSize) 
    {
        speedFactor *= 1.1f;
    }
    else
    {
        speedFactor *= 0.9f;
    }

    t += speedFactor * Time.deltaTime;
}

尽管该解决方案与曲线函数无关,但是我想在此处注意这一点,因为我也在寻找如何在Bezier曲线上实现恒定速度,然后提出了该解决方案。考虑到该功能的普及,这可能会有所帮助。


-3

我对cocos2一无所知,但是贝塞尔曲线是一种参数方程式,因此您应该能够获得时间上的x和y值。


4
添加示例+更多说明,这将是一个很好的答案。
MichaelHouse
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.