Як досягти рівномірної швидкості руху по кривій Безьє?


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ΔtLL÷dMdT

Застосування: квадратична крива Безьє

Якщо контрольними точками кривої Безьє є , і , траєкторію можна виразити так:ABC

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

Отже, похідна:

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

Вам просто потрібно десь зберігати вектори і . Тоді для заданого , якщо ви хочете просунути довжину , зробіть:v1=2A4B+2Cv2=2A+2BtL

t=t+Llength(tv1+v2)

Кубічні криві Безьє

Це ж міркування стосується кривої з чотирма контрольними точками , , і :ABCD

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)

Питання точності

Якщо ви працюєте з розумним кадром, (який слід обчислити відповідно до тривалості кадру) буде достатньо малим для наближення до роботи.L

Однак у крайніх випадках у вас можуть виникнути неточності. Якщо занадто великий, ви можете зробити обчислення кусочно, наприклад, використовуючи 10 частин:L

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

1
Привіт. Я читаю вашу відповідь, але не можу зрозуміти, що таке Л. Що ви маєте на увазі під "що слід обчислити відповідно до тривалості кадру"?
Михайло IV

Чи довжина відрізка кривої?
Михайло IV

L - довжина кривої, тобто відстань, яку потрібно пройти протягом поточного кадру.
sam hocevar

Гаразд, зараз я бачу. І ви вважаєте, що це наближення настільки ж добре, як і техніка розщеплення кривих, з відповіді нижче?
Михайло IV

Якщо Lдостатньо мало, це наближення завжди точніше, ніж відповідь нижче, так. Він також використовує менше пам'яті (тому що використовує похідну, а не зберігає всі значення точки). Коли Lпочинає рости, ви можете скористатися технікою, яку я пропоную наприкінці.
Сем Хочевар

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;
}

Хоча рішення не залежить від функції кривої, я хотів зазначити це тут, оскільки я також шукав, як досягти постійної швидкості на кривій Безьє, і тоді я придумав це рішення. Враховуючи популярність функції, це може бути корисним тут.


-3

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


4
Додайте приклад + більше пояснень, і це буде гарною відповіддю.
MichaelHouse
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.