Vamos aprender XNA? – Parte 21 – Keyframed animation

Publicado em 05/11/2011

0


Olá pessoal. Tudo certo!?

Nesse post, continuo falando sobre “animação” em XNA. No post anterior, mostrei como construir animações lineares simples. Agora, vou um pouco além: mostro como criar animações mais complexas usando uma técnica chamada “Keyframed animation”.

Funciona assim: adicionamos “key” frames onde estabelecemos a rotação e a posição que desejamos que o modelo esteja em determinado tempo de execução.

O código-fonte completo dessa série está disponível em https://github.com/ElemarJR/VamosAprenderXNA. No post, apresento apenas os trechos de código mais relevantes.

Representando Key Frames

Como disse, os “key” frames definem uma posição e rotação para um objeto em um determinado instante de animação. Observe:

class ModelAnimationKeyframe
{
    public Vector3 Position { get; private set; }
    public Vector3 Rotation { get; private set; }
    public TimeSpan Time { get; private set; }

    public ModelAnimationKeyframe(
        Vector3 position,
        Vector3 rotation,
        TimeSpan time
        )
    {
        this.Position = position;
        this.Rotation = rotation;
        this.Time = time;
    }
}

De forma simples, para definirmos os dados necessários para a animação, bastaria uma lista simples de Key frames, onde, o primeiro item, estivesse no tempo zero. Para tornar a criação dessa lista mais simples e intuitiva, criei uma coleção especializada. Observe:

class ModelAnimationKeyframeList : List<ModelAnimationKeyframe>
{
    public ModelAnimationKeyframeList(
        Vector3 startposition,
        Vector3 startrotation)
    {
        this.AddStep(
            startposition,
            startrotation,
            TimeSpan.FromSeconds(0)
            );
    }
        
    public ModelAnimationKeyframeList Translate(
        float dx = 0, float dy = 0, float dz = 0, int deltaseconds = 2
        )
    {
        return this.AddStep(
            new Vector3(dx, dy, dz),
            Vector3.Zero,
            deltaseconds
        );
    }

    public ModelAnimationKeyframeList Rotate(
        float dx = 0, float dy = 0, float dz = 0, int deltaseconds = 2
        )
    {
        return this.AddStep(
            Vector3.Zero,
            new Vector3(dx, dy, dz),
            deltaseconds
        );
    }

    public ModelAnimationKeyframeList AddStep(
        Vector3 deltaposition,
        Vector3 deltarotation,
        int deltaseconds = 2
        )
    {
        return this.AddStep(deltaposition, deltarotation, TimeSpan.FromSeconds(deltaseconds));
    }

    public ModelAnimationKeyframeList AddStep(
        Vector3 deltaposition,
        Vector3 deltarotation,
        TimeSpan deltatime
        )
    {
        if (this.Count == 0)
            this.Add(new ModelAnimationKeyframe(
                deltaposition,
                deltarotation, 
                deltatime)
                );
        else
        {
            var previous = this.Last();
            this.Add(new ModelAnimationKeyframe(
                previous.Position + deltaposition,
                previous.Rotation + deltarotation,
                previous.Time +  deltatime)
                );
        }
        return this;
    }

    public int IndexOf(TimeSpan offset)
    {
        if (offset > this.Last().Time) return -1;
        if (offset < this.First().Time) return -1;
            
        var result = 0;
        while (this[result].Time <= offset)
            result++;
        return result;
    }
}

Como pode perceber, alterei o construtor para definir a posição e rotação para o instante zero. Fora isso, defini alguns métodos “facilitadores” que permitem a criação de frames a partir de “deltas” para as posições e rotações anteriores.

Além disso, defini um IndexOf “turbinado” que identifica o índice do keyframe que estaria sendo processado em um determinado deslocamento de tempo.

Agora, perceba como é simples criar uma lista de keyframes para animação:

var list = new ModelAnimationKeyframeList(spaceship.Position, spaceship.Rotation)
        .Translate(dz: 1600)
        .Rotate(dy: MathHelper.PiOver2)
        .Translate(dx: 1600)
        .Rotate(dy: MathHelper.PiOver2)
        .Translate(dz: -1600)
        .Rotate(dy: MathHelper.PiOver2)
        .Translate(dx: -1600)
        .Rotate(dy: MathHelper.PiOver2);

Como pode ver, esse conjunto de keyframes define um movimento “quadrado”. Avançando 1600 unidades, virando 90 graus para esquerda, mais 1600 unidades …

Executando um Keyframed animation

Agora que já sabemos como definir os “quadros”, vamos ver como executar a animação. Observe:

class KeyframedModelAnimation
{

    public ModelAnimationKeyframeList Frames
    { get; private set; }

    public Vector3 Position
    { get; private set; }

    public Vector3 Rotation
    { get; private set; }

    public TimeSpan ElapsedTime
    { get; private set; }

    public bool Looping
    { get; private set; }

    public bool AutoReverse
    { get; private set; }

    public KeyframedModelAnimation(
        ModelAnimationKeyframeList frames,
        bool looping = true,
        bool autoreverse = true
        )
    {
        this.Frames = frames;
        this.Looping = looping;
        this.ElapsedTime = TimeSpan.FromSeconds(0);
        this.AutoReverse = autoreverse;
    }

    bool reversing = false;
    public void Update(TimeSpan elapsed)
    {
        this.ElapsedTime += elapsed;
        var last = this.Frames.Last();

        if (this.ElapsedTime > last.Time && !this.Looping)
        {
            this.Position = last.Position;
            this.Rotation = last.Rotation;
        }
        else
        {
            if (this.ElapsedTime > last.Time)
            {
                while (this.ElapsedTime > last.Time)
                    this.ElapsedTime -= last.Time;
                reversing = !reversing && this.AutoReverse;
            }

            int index;
            ModelAnimationKeyframe previous, target;
            TimeSpan time;

            if (!reversing)
            {
                time = this.ElapsedTime;
                index = this.Frames.IndexOf(time);
                previous = this.Frames[index - 1];
                target = this.Frames[index];
            }
            else
            {
                time = last.Time - this.ElapsedTime;
                index = this.Frames.IndexOf(time);
                previous = this.Frames[index];
                target = this.Frames[index - 1];
            }

            var currentFrameTime = time - previous.Time;
            var endFrameTime = target.Time - previous.Time;

            var amount = (float)currentFrameTime.TotalSeconds /
                (float)endFrameTime.TotalSeconds;

            this.Position = Vector3.Lerp(previous.Position, target.Position, amount);
            this.Rotation = Vector3.Lerp(previous.Rotation, target.Rotation, amount);
        }
    }
}

Toda a lógica é semelhante a explicada no post anterior. A novidade, foi algum “malabarismo” para isolar a “parte da animação” que estamos executando. De qualquer forma, acho que o código é auto-explicativo.

Era isso.

Smiley piscando

Etiquetado:,
Publicado em: Post