Como obter velocidade uniforme de movimento em uma curva bezier?

22

Estou tentando mover uma imagem ao longo da curva de Bezier. É assim que eu faço:

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

}

Meu problema é que a imagem não se move uniformemente. No começo, ele está se movendo lentamente e depois acelera gradualmente e, no final, está se movendo muito rápido. O que devo fazer para me livrar dessa aceleração?

Andrey Chernukha
fonte

Respostas:

27

É possível aproximar uma solução para esse problema para a maioria das trajetórias paramétricas. A idéia é a seguinte: se você aplicar zoom suficientemente profundo em uma curva, não poderá distinguir a curva da tangente nesse ponto.

Ao fazer essa suposição, não há necessidade de precompor nada mais do que dois vetores (três para curvas de Bezier cúbicas etc. ).

Portanto, para uma curva , calculamos seu vetor tangente no ponto . A norma desse vetor é e, portanto, a distância percorrida por uma duração pode ser aproximada como . Segue-se que uma distância é percorrida por uma duração .M(t)dMdttdMdTΔtdMdTΔtLL÷dMdT

Aplicação: curva quadrática de Bezier

Se os pontos de controle da curva de Bezier são , e , a trajetória pode ser expressa como:ABC

M(t)=(1-t)2UMA+2t(1-t)B+t2C=t2(UMA-2B+C)+t(-2UMA+2B)+UMA

Portanto, a derivada é:

dMdt=t(2UMA-4B+2C)+(-2UMA+2B)

Você só precisa armazenar vetores e algum lugar. Então, para um dado , se você quiser avançar um comprimento , faça:v1=2UMA-4B+2Cv2=-2UMA+2Bteu

t=t+eueuength(tv1+v2)

Curvas cúbicas de Bezier

O mesmo raciocínio se aplica a uma curva com quatro pontos de controle , , e :UMABCD

M(t)=(1-t)3UMA+3t(1-t)2B+3t2(1-t)C+t3D=t3(-UMA+3B-3C+D)+t2(3UMA-6B+3C)+t(-3UMA+3B)+UMA

A derivada é:

dMdt=t2(-3UMA+9B-9C+3D)+t(6UMA-12B+6C)+(-3UMA+3B)

Pré-calculamos os três vetores:

v1=-3UMA+9B-9C+3Dv2=6UMA-12B+6Cv3=-3UMA+3B

e a fórmula final é:

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

Problemas de precisão

Se você estiver executando em uma taxa de quadros razoável, (que deve ser calculado de acordo com a duração do quadro) será suficientemente pequeno para que a aproximação funcione.eu

No entanto, você pode enfrentar imprecisões em casos extremos. Se for muito grande, você poderá fazer o cálculo por partes, por exemplo, usando 10 partes:eu

for (int i = 0; i < 10; i++)
    t = t + (L / 10) / length(t * v1 + v2);
sam hocevar
fonte
1
Oi. Estou lendo sua resposta, mas não consigo entender o que é L. O que você quer dizer com "que deve ser calculado de acordo com a duração do quadro"?
Michael IV
L = comprimento do segmento da curva?
Michael IV
L é o comprimento da curva, ou seja, a distância que você deseja percorrer durante o quadro atual.
Sam Hocevar
OK, eu vejo agora. E você acha que essa aproximação é tão boa quanto a técnica de divisão de curvas da resposta abaixo?
Michael IV
Quando Lé suficientemente pequena, essa aproximação é realmente sempre mais precisa do que a resposta abaixo, sim. Ele também usa menos memória (porque usa a derivada em vez de armazenar todos os valores de pontos). Quando Lcomeçar a crescer, você pode usar a técnica que sugiro no final.
sam hocevar
6

Você precisa reparamaterizar a curva. A maneira mais fácil de fazer isso é calcular os comprimentos do arco de vários segmentos da curva e usá-los para descobrir de onde você deve amostrar. Por exemplo, talvez em t = 0,5 (no meio do caminho), você deve passar s = 0,7 para a curva para obter a posição "no meio do caminho". Você precisa armazenar uma lista de comprimentos de arco de vários segmentos de curva para fazer isso.

Provavelmente existem maneiras melhores, mas aqui está um código C # muito simples que escrevi para fazer isso no meu jogo. Deve ser fácil portar para o Objetivo 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: Vale a pena notar que isso não fornecerá o comprimento exato do arco, pois é impossível obter o comprimento do arco de uma curva cúbica. Tudo o que isso faz é estimar o comprimento dos vários segmentos. Dependendo do comprimento da curva, pode ser necessário aumentar a resolução para impedir que ela mude de velocidade quando atingir um novo segmento. Eu costumo usar ~ 100, com o qual nunca tive problemas.

Robert Fraser
fonte
0

Uma solução muito leve é ​​aproximar a velocidade em vez de aproximar a curva. Na verdade, essa abordagem é independente da função da curva e permite que você use qualquer curva exata em vez de usar derivadas ou aproximações.

Aqui está o código para o 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;
}

Embora a solução seja independente da função de curva, eu queria anotá-la aqui, pois também estava procurando como obter velocidade constante em uma curva de Bezier e, em seguida, forneço essa solução. Considerando a popularidade da função, isso pode ser útil aqui.

Guney Ozsan
fonte
-3

Eu não sei nada sobre cocos2, mas uma curva de bezier é um tipo de equação paramétrica, portanto, você poderá obter seus valores xey em termos de tempo.

Jebbles
fonte
4
Adicione um exemplo + mais explicação e essa seria uma boa resposta.
MichaelHouse