Как добиться равномерной скорости движения по кривой Безье?

22

Я пытаюсь переместить изображение по кривой Безье. Вот как я это делаю:

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

}

Моя проблема в том, что изображение движется неравномерно. Вначале он движется медленно, а затем постепенно ускоряется, и в конце он движется очень быстро. Что я должен сделать, чтобы избавиться от этого ускорения?

Андрей Чернуха
источник

Ответы:

27

Решение этой задачи можно аппроксимировать для большинства параметрических траекторий. Идея заключается в следующем: если вы достаточно сильно приблизитесь к кривой, вы не сможете отличить саму кривую от ее касательной в этой точке.

Делая это предположение, нет необходимости предварительно вычислять что-либо больше, чем два вектора (три для кубических кривых Безье и т . Д. ).

Таким образом, для кривой M(t) мы вычисляем ее касательный вектор dMdt в точкеt. Норма этого вектораdMdTиследовательнорасстояниепройденноетечение длительностиΔtможет быть аппроксимировано какdMdTΔt. Отсюда следует, что расстояниеLпреодолевается за времяL÷dMdT.

Применение: квадратичная кривая Безье

Если контрольными точками кривой Безье являются A , B и С , траектория может быть выражена как:

M(t)=(1t)2A+2t(1t)B+t2C=t2(A2B+C)+t(2A+2B)+A

Таким образом, производная:

dMdt=t(2A4B+2C)+(2A+2B)

Вам просто нужно хранить векторы v1=2A4B+2C и v2=2A+2В где-нибудь. Затем для заданного t , если вы хотите продвинуться на длину L , вы делаете:

t=t+Llength(tv1+v2)

Кубические кривые Безье

То же самое относится и к кривой с четырьмя контрольными точками A , B , C и D :

M(t)=(1t)3A+3t(1t)2B+3t2(1t)C+t3D=t3(A+3B3C+D)+t2(3A6B+3C)+t(3A+3B)+A

Производная это:

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

Мы предварительно вычислим три вектора:

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

и окончательная формула:

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

Accuracy issues

If you are running at a reasonable framerate, L (which should be computed according to the frame duration) will be sufficiently small for the approximation to work.

However, you may experience inaccuracies in extreme cases. If L is too large, you can do the computation piecewise, for instance using 10 parts:

for (int i = 0; i < 10; i++)
    t = t + (L / 10) / length(t * v1 + v2);
sam hocevar
источник
1
Hi. I am reading your answer,but I can't understand what is L. What do you mean by "which should be computed according to the frame duration"?
Michael IV
Is L = curve segment length?
Michael IV
L is the curve length, i.e. the distance you want to travel during the current frame.
sam hocevar
OK,I see now. And you think this approximation is as good as curve splitting technique from the answer below?
Michael IV
When L is sufficiently small, this approximation is actually always more accurate than the answer below, yes. It also uses less memory (because it uses the derivative instead of storing all point values). When L starts to grow, you can use the technique I suggest at the end.
sam hocevar
6

You need to reparamaterize the curve. The easiest way to do this is to calculate the arc lengths of several segments of the curve and use these to figure out where you should sample from. For example, maybe at t=0.5 (halfway through), you should pass s=0.7 to the curve to get the "halfway" position. You need to store a list of arc lengths of various curve segments to do this.

There are probably better ways, but here's some very simple C# code I wrote to do this in my game. It should be easy to port to Objective 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);
    }
}

Edit: It's worth noting that this won't give you the exact arc length, since it's impossible to get the arc length of a cubic curve. All this does is estimate the length of the various segments. Depending on how long the curve is, you may need to increase the resolution to prevent it from changing speeds when it reaches a new segment. I usually use ~100, which I have never had a problem with.

Robert Fraser
источник
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;
}

Хотя решение не зависит от функции кривой, я хотел бы отметить это здесь, так как я также искал, как достичь постоянной скорости на кривой Безье, и затем я придумаю это решение. Учитывая популярность функции, это может быть полезно здесь.

Гуней Озсан
источник
-3

Я ничего не знаю о cocos2, но кривая Безье - это некое параметрическое уравнение, поэтому вы должны иметь возможность получать значения x и y в терминах времени.

Jebbles
источник
4
Добавьте пример + больше объяснений, и это будет хорошим ответом.
MichaelHouse