Skip to content
Snippets Groups Projects
Forked from lifapc / freshfish-etu
Source project has a limited visibility.
user avatar
Alexandre authored
826b1a9f
History

M1 Animation Personnage

Instructions de compilation

En principe, ce sont les mêmes que pour la compilation du projet initial sans mes modifications. Je n'ai ajouté des fichiers que dans le répertoire du TP, et donc tout devrait fonctionner avec premake.

Squelette

Le squelette est calqué sur la structure de données donnée dans le sujet du TP. On peut initialiser le squelette à partir d'un BVH mais surtout définir la pose comme un numéro de frame, ou comme une interpolation entre une frame d'un BVH avec une frame d'un autre (qui peut être le même) BVH.

Déplacement du personnage

Tout est géré par le CharacterController. La logique de déplacement n'est pas compliquée : stockage de la vitesse, vitesse max, et direction du personnage. A chaque update, on met à jour la position en fonction de la vitesse. On peut bien sûr modifier la vitesse avec les accesseurs. On peut par la suite récupérer la position et la direction vers l'avant du personnage, pour afficher le modèle au bon endroit et dans la bonne orientation.

Animation

Objectif

L'animation est faite par une machine à états "AnimationStateMachine" (abrégé ensuite ASM). Je voulais que mon ASM puisse permettre de stocker autant d'animations que l'on souhaite : une par état, mais aussi autant de transtions que l'on souhaite tant qu'on ne dépasse pas une transition par paire orientée d'animations (c'est à dire que si A et B sont des animations, on peut avoir maximum une transtion de A vers B). Je voulais que les transitions soient traitées par l'ASM lui-même, c'est à dire que c'est à l'ASM de décider si il est possible de passer d'un état à un autre en fonction des comment a été paramétré la transition lors de sa définition.

Enfin, l'ASM est capable de définir la pose d'un squelette, puisque c'est l'ASM qui contient toutes les informations permettant de décider de la pose.

API

  • Ajout d'une animation
    • Par son adresse sur le disque ou par un objet BVH déjà créé.
    • Avec un nom (identifiant unique), soit choisi par l'utilisateur, soit identique au chemin sur le disque.
  • Ajout d'une transition
    • Entre deux animations ayant déjà été ajoutées à l'ASM.
    • Avec un foncteur, une lambda, une fonction orpheline (un functional au sens de la librairie standard), renvoyant vrai si la transition est possible, faux sinon.
    • En définissant un "temps de transition" (blending), indiquant pendant combien de temps on souhaite que l'ASM utilise l'animation de départ et l'animation d'arrivée en même temps (interpolation linéaire entre ces deux animations pendant tout le temps de transition).
  • Définition d'une animation de départ
    • Avec l'identifiant d'une animation ajoutée à l'ASM
  • Une fonction de démarrage (start)
    • Afin de lancer la paramétrisation initiale de l'ASM
    • Nécessite qu'une animation de départ ait été choisie (sinon, runtime_error)
  • Une fonction update (à chaque frame)
    • Afin de garder une trace du temps écoulé
    • Afin de lancer le mécanisme de transtion lorsque cela est possible.
    • Afin de garder un oeil sur le "mixage" (blending) entre deux états/animations (lors d'une transition, mélange de deux animations).
  • Une fonction permettant d'appliquer la bonne pose au squelette
    • Au moyen de toutes les données calculées dans les fonctions énoncées précédemment
    • On souhaitera que le squelette ait une pose en continuité avec la frame précédente, soit dans une même animation, soit lors de la transition entre deux animations.

Implémentation

Le stockage des animations est fait dans une hashmap, où l'identifiant sous forme de chaîne de caractères permet d'accéder à l'animation (chara::BVH).

Le stockage des transtions est sous la forme d'une double hasmap : Hashmap<String, Hashmap<String, Link>>, où Link est une nest class de l'ASM contenant les noms des animations de départ et d'arrivée, le std::functional renvoyant vrai lorsque la transition est possible, et le temps de transition. Cette structure de données, bien qu'un peu barbare (elle aurait pu bénéficier d'une classe à part entière pour que les accesseurs soient plus facilement lisibles, mais j'ai manqué de temps), permet d'éviter d'itérer dans un tableau de Link : on peut directement et en temps constant accéder à l'animation de départ, ce qui est important, puisqu'on cherche à chaque frame tous les liens dont l'animation courante est l'animation de départ. En y repensant, la Hashmap interne aurait pu être une simple std::list, économisant de la mémoire et améliorant la lisibilité du code, puisqu'on souhaite uniquement itérer à l'intérieur pour connaître les transitions possibles depuis l'animation courante.

On a un stockage de l'animation de départ, de l'animation courante, et de l'animation précédente sous forme d'itérateurs sur le container des animations. Le stockage de l'animation précédente est important puisqu'on souhaite (la plupart du temps) faire une transition fluide entre deux animations. Il faut donc connaître l'ancienne animation pour faire la transition.

Il est aussi nécessaire de garder une trace du temps écoulé, ce qui est fait dans une variable "m_EllapsedTimeSinceLastAnim". Cette variable stocke le temps écoulé depuis que l'animation courante a démarré. Ceci permet de savoir entre quelles frames consécutives l'on se trouve à tout instant, et ainsi assurer l'affichage d'une pose interpolée entre deux frames. Dans la continuité du paragraphe précédent, on stocke également le temps écoulé entre le début de l'animation précédente et la fin de l'animation précédente (coïncidant avec le début de l'animation courante). Ceci permet de savoir à quelle frame l'on s'était arrêté dans l'animation précédente.

Scénario d'une transition

Plaçons-nous dans le cas d'une transition de l'animation A vers l'animation B durant 1 seconde et déroulons la logique de l'ASM :

  • Nous somme dans l'état (animation) A.
  • La fonction updateSkeleton, permettant de placer le skelette dans la bonne pose, va placer à chaque tour de boucle le skelette au bon endroit entre deux frames interpolées.
  • Dans la fonction update,
    • nous mettons à jour le temps écoulé depuis que l'état A a démarré (grâce au delta time),
    • nous vérifions si une transition est possible. Dans notre scénario, la fonction de transition du lien A -> B retourne vrai, et donc la transition vers B est possible. La fonction update va appeler une fonction interne démarrant le mécanisme de transtion (playAnimationWithBlend()).
  • Dans la fonction playAnimationWithBlend(),
    • l'animation précédente devient l'animation courante (A),
    • l'animation courante devient B,
    • le temps de transition courant devient le temps de transition du lien A -> B (1 seconde),
    • l'ancien temps écoulé devient le temps écoulé courant (le temps depuis que A a démarré),
    • le temps écoulé courant devient zéro.

Maintenant, nous avons en mémoire toutes les données permettant de dire que nous sommes en processus de transition ou non. En effet, si le temps écoulé courant, donc le temps écoulé depuis le début de B est inférieur au temps de transition (1 seconde), nous sommes en période de transition, et il faudra afficher une pose mélangée entre A et B. Au tout début, on a donc 0 seconde < 1 seconde, car B vient de démarrer (0 seconde) et la dernière transition démarrée (A -> B) dure 1 seconde. Au fil des exécutions de update(), le temps écoulé dépasseré 1 seconde, et on ne sera plus en transition. De plus, nous savons quand s'est arrêtée A, et nous savons donc à quelle frame s'est arrêtée A (grâce à quelques opérations de modulo entre le temps qu'a duré A, le nombre de frames de A et la durée d'une frame de A). Enfin, nous savons le temps écoulé depuis le début de B (mis à jour dans la fonction update()), nous savons donc quelle est la frame courante de B avec la même logique des opérations modulo.

  • Dans la fonction updateSkeleton, le comportement est différent car nous détectons une transition en cours.
    • On va déterminer à quel pourcentage de la transition nous sommes, en divisant le temps depuis le début de B par le temps de transition.
    • Ce pourcentage servira de coefficient d'interpolation entre A dans sa dernière frame affichée, et B dans la frame courante, dont l'obtention est spécifiée ci-dessus.
    • On utilisera la méthode du squelette permettant d'interpoler deux frames de deux BVH différents avec un coefficient d'interpolation.

Lorsque la transition sera terminée (quand le temps depuis le début de B dépassera 1 seconde), updateSkeleton reprendra son comportement d'affichage d'une seule animation, et va poser le skelette dans une interpolation entre deux frames successives de B, selon le temps écoulé depuis le début de B.

Tout ce schéma se répétera indéfiniment selon les transitions possibles ou non.

Physique

Pour la collision entre le personnage et les particules, on vérifie pour chaque articulation s'il y a collision avec chaque particule. Si c'est le cas, on déplace la particule pour qu'elle ne soit plus en superposition avec l'articulation, et on lui applique une force dans la direction articulation -> particule, avec une force définie dans la classe particule ("force" du personnage).

Pour la collision entre les particules, à chaque update du monde physique on itère sur toutes les paires (A, B) avec A et B des particules différentes pour résoudre la collision. Si on a résolu la collision entre A et B, on ne résout pas la collision entre B et A. La résolution de la collision se fait avec une formule de résolution de choc entre deux masses ponctuelles m1 et m2. Elle repose sur le fait que la quantité de mouvement ainsi que l'énergie cinétique totale d'un système fermé sont constantes. Dans le cas d'un choc entre deux particules, le système n'est pas fermé car il y a gestion de la friction, cependant, aux abords du choc, on peut considérer le système comme fermé car la friction est négligeable. Donc aux abords du choc, la quantité de mouvement et l'énergie cinétique totale sont constantes. Ceci permet d'établir deux systèmes dont les inconnues sont les vitesses finales v1f et v2f. On pourra ensuite définir v1f et v2f en fonction de m1, m2, v1i et V2i, où v1i et v2i sont les vitesses initiales des particules, et m1 et m2 sont leurs masses. On doit également introduire un vecteur unitaire allant de de la particule 1 à la particule 2, permettant de réfléchir la direction des deux particules par rapport à l'axe passant par les deux particules. Ce vecteur traduit le rebond d'une particule sur l'autre, comme deux boules de billard. Je n'ai pas recalculé ces deux formules moi-même, car mes cours de physique des chocs remontent à la L1 et je n'avais pas le temps de me réimprégner des méthodes permettant de retrouver les formules des vitesses finales.

Scène et contrôles

  • On a dix particules qui tombent sur le sol au début de la simulation. Leurs tailles et masses sont aléatoirement choisies dans un intervalle.
  • Animations du personnages (7) :
    • Locomotion (déplacement avec les touches ZQSD)
      • Idle
      • Marche
      • Course
    • Coup de pied en appuyant sur x
    • Saut en appuyant sur espace
      • "pré-saut" pour préparer au saut
      • saut en lui-même (chute libre)
      • atterrissage

Améliorations possibles

  • Je trouve que le couplage entre le squelette et l'ASM reste relativement fort. Peut-être serait-il judicieux d'introduire une classe faisant l'interface entre ASM et squelette, et qui calculerait la pose du squelette selon l'état de l'ASM. Cependant, cela ajouterait un étage d'indirection (car il faudrait récupérer toutes les données de l'ASM), et cela peut être problématique dans le contexte d'une application temps réel où le nombre de squelettes animées peut potentiellement être élevé. Je ne sais pas dans quelle mesure cela pourrait ralentir le programme, mais son intérêt pour la lisibilité du code ne fait pas de doute.
  • Les animations sont très fluides lorsqu'on affiche une seule animation (non mixée). Cependant, lors d'une transition, on perd en fluidité. En effet, lors de la transition, on fait une interpolation entre une frame i d'une animation A et une frame j d'une animation B avec un coefficient d'interpolation w, où i, A, B sont constants et j progresse incrémentiellement. w progresse de façon fluide et linéaire par rapport au temps, mais j non, ce qui implique un petit saut à chaque frame de B.
    • Introduire la possibilité d'interpoler AUSSI deux poses entre elles, et non deux BVH, permettrait probablement de résoudre ce problème. Ainsi, pour une transition, on pourra récupérer la POSE exacte lorsque A s'est terminée (et non sa FRAME), et la POSE exacte de B à l'instant t (et non sa FRAME). Pour la pose de B, elle peut déjà être obtenue avec l'interpolation entre deux frames consécutives de B (l'ASM le fait déjà hors période de transition). Ensuite, on ferait l'interpolation entre ces deux poses. On pourra garder en mémoire la pose du squelette quand A s'est terminée afin de ne pas la recalculer à chaque frame. Le recalcul de la pose dans l'animation B à chaque frame est inévitable dans cette solution (on le fait déjà hors période de transition).