<= Retour vers La page principale du cours




TP Animation de personnage avec Unity



Préambule : (Important)

La partie A de ce TP n'est pas prioritaire. Elle sert à prendre en main Unity et à illustrer les mécanismes classiques et “sans code” d'animations avec les moteurs de jeu vidéo. La partie A doit occuper le 1er TP d'1h30. La partie importante est la partie B qui peut se commencer dès le CM2 “Cinématique inverse”. La partie C n'est pas à faire, sauf si vous vous ennuyez 8-)


Unity : les bases


Créez un nouveau projet et une nouvelle scène. Sauvez bien la scène et le projet.

  • Ajoutez un plan qui servira de terrain : menu “GameObject”, “3D”, “Plane”
  • Bougez la caméra avec la souris + alt (rotation) ou ctrl+alt (Translation)
  • Sélectionnez un objet (ou créez-en un), les touches pour changer de mode
    • Q Pan
    • W Move (Translation de l'objet)
    • E Rotate (Rotation de l'objet)
    • R Scale (Changement d'échelle de l'objet)


Partie A : Animation et MecAnim


A.1. Contrôleur de déplacement d'une capsule au clavier/souris

L'objectif de cette partie est de piloter un objet au clavier/souris.

Une vidéo montrant les manipulations (faites des pauses)

Le faire bouger

  • Créez un objet simple que nous allons piloter, par exemple une capsule. Menu “GameObject” puis “3D Object” puis “Capsule”
  • Pour que notre objet fasse quelque chose, nous allons devoir écrire du code pour lui dire quoi faire. Dans le panneau Project, sélectionnez Createen haut (ou cliquez avec le bouton droit de la souris n'importe où dans le panneau) et sélectionnez C# script. Nommez-le CharacterBasicControler.
  • Double-cliquez sur votre script pour l'ouvrir (Visual Studio se lance). Chaque fois que vous créez un nouveau script, Unity ajoute deux méthodes par défaut, Start () et Update (). Start est appelé une fois, lorsque l'objet entre dans le monde du jeu. La mise à jour est appelée à chaque image/rendu.
  • Copiez et collez la ligne suivante dans la méthode de mise à jour (Update).
transform. position += Vector3.forward * Time.deltaTime;
  • Sélectionnez votre capsule en cliquant dessus. A droite dans le panel “Inspector”, Ajoutez un composant “Add component”, puis trouvez votre script de mouvement dans la petite barre de recherche (ou section “script”) et ajoutez-le à votre capsule.
  • Appuyez sur le bouton de lecture en haut de l'éditeur.
  • Vous pouvez ajouter une variable de class “speed”
public float speed = 10.0f;
  • Et modifiez le script. Notez que la variable “speed” est accessible aussi dans le panel “Inspector” car elle est public.
transform.position += Vector3.forward * speed * Time.deltaTime;


Contrôler la direction

  • Renommez votre capsule en “Character”, ce sera plus pertinent.
  • Modifiez la fonction update
        Vector3 forward_world = transform.TransformDirection(Vector3.forward);
        transform.position += forward_world * speed * Time.deltaTime;
  • Decochez le “Capsule Collider” dans le panel “Inspector” à droite.
  • Ajoutez un “Character Controller” avec le menu “Add component”
  • Ouvrez votre script “CharacterBasicControler” et changez ceci
gameObject.GetComponent<CharacterController>().Move(forward_world * speed * Time.deltaTime);
  • Vous devriez garder le même comportement qu'avant mais maintenant c'est le Character Controller qui pilote votre Character.
  • Ajoutez un cube qui servira de nez à la capsule pour savoir dans quelle direction il avance. Ce cube doit être un fils de “Character”, faites glissez “nose” dans “Character” à gauche dans la hiérarchie de scène.
  • Dans le script, ajoutez ces lignes qui récupèrent la souris et font tourner le Character (vous pouvez mettre speed à 0.1 dans l'interface ou dans la fonction “start” pour ralentir le mouvement)
    Vector2 mouseInput = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    transform.Rotate(Vector3.up, mouseInput.x * rotationSpeed);
  • Modifiez la ligne du move par celle-ci pour utiliser le champ input
gameObject.GetComponent<CharacterController>().Move(transform.TransformDirection(input * speed * Time.deltaTime));
  • Déplacez la caméra principale pour qu'elle devienne un enfant du Character (ou du nose). De cette façon, la caméra suivra le Character.


A faire sans guide

Vous pouvez enrichir votre projet.

  • Tourner seulement si la touche “contrôle” est enfoncée
  • Accélérer/freiner/tourner avec les touches du claviers
  • Ajoutez une rotation verticale pour pouvoir regarder en haut
  • Activez les collisions entre votre capsule/perso et des cubes posés dans la scène
  • Ajoutez des possibilités de tir de sphères droit devant

Ici, le tuto qui a inspiré cette partie du TP





A.2. Premier pas avec MecAnim

Un tutorial simple d'animation d'un ventilateur avec MecAnim. La vidéo ci-dessous montre la création d'animation d'une hélice avec des keyframes. La suite (A.3) se propose d'animer un personnage humanoïde en important des données depuis des fichiers de motion capture.



A.3. Contrôleur simple d'un personnage animé avec MecAnim



Les données : Mixamo

Pour aller plus vite, vous pouvez télécharger le zip ici.


Mixamo est un site offrant des personnages (Maillage) et des animations.

  • Allez dans l'onglet “Character” sélectionnez le personnage qui vous intéresse.
  • Allez dans l'onglet “Animation”, tapez “locomotion pack” dans le moteur de recherche pour trouver un pack d'animation. Cliquez sur celui qui vous intéresse.
  • Cliquez à droite sur “download”. Le zip doit contenir le maillage et les animations.


Il y a également des données dans https://www.assetstore.unity3d.com/l'assets store d'Unity.



Importer les données dans Unity

  • Votre personnage va avoir de nombreux composants différents, il est important de créer un nouveau dossier. Menu Assets > Create > Folder, nommez le “Perso”.
  • Importer vos animations en faisant glisser/déposer depuis l'explorateur (ATTENTION : ne pas faire import qui ne prendra pas les fichier meta)
  • Selectionnez toutes les animations, puis dans le panel “Inspector” à droite cliquez sur le bouton “Rig”

  • et mettez le mode “humanoid” dans “Animation Type” (qui y est peut-être déjà), à faire également pour le mesh du personnage.



Contrôleur d'animation

  • Bouton droit, créez un “Animation Controller” nommé par exemple “MyAniCon”.
  • Créez un script C#, nommé “MyAniConScript”.
  • Placez votre personnage sur la scène, et sélectionnez le personnage.
  • Dans le panel “inspector” à droite, placez le contrôleur d'animation “MyAniCon” dans Animator/Controller
  • Puis faites “add component” pour ajouter le “MyAniConScript”

  • Fenêtre “Animator”, clique droit puis “Créer > Nouveau Blendtree”

  • Dans le coin supérieur gauche de la fenêtre “Animator”, sélectionnez l'onglet Paramètres et créez un

nouveau paramètre Float et nommez-le VSpeed

  • Fenêtre Animateur, double-cliquez sur IdleWalk. Réglez le paramètre sur VSpeed. Cliquez sur le bouton '+' et dans Actifs,Animations, faites glisser l'animation Idle dans le dossier Animations Motion Field.
  • Répétez le processus et faites glisser l'animation de marche dans le deuxième champ Animation.

  • Votre BlendTree doit ressembler à ceci

  • Pour passer d'un état à un autre nous avons besoin d'un script C#. Ouvrez MyAniConScript.cs

et indiquez ceci

    void Start () {
        myAnimator = GetComponent<Animator>();
        Debug.Log("MyAniConScript: start => Animator");
    }
	
	// Update is called once per frame
	void Update () {
        myAnimator.SetFloat("vSpeed", Input.GetAxis("Vertical"));
        Debug.Log("vSpeed = " + Input.GetAxis("Vertical"));
    }
  • Ca bouge !!! mais seulement une fois (enfin ca dépend de vos animations d'entrée) … Allez dans le “Blendtree” et pour chaque animation coché “Loop time”


Droite et gauche

  • Ajoutez un paramètre hSpeed
  • Améliorer votre Blend Tree pour qu'il prenne en compte les virages à gauche/droite, la course, etc. Ne mettez dans le blendtree que les déplacements classiques.
  • Dans l'“Inspector” passer le système d'interpolation en motion field “2D Freeform Cartesian”. Le centre va correspondre à Idle, les points à droite au animation tournant à droite, ceux à gauche les virages à gauche et les points en haut pour les marches et courses.
  • Remarque : les données ne comportent que des animation “tourner à droite”, vous pouvez les inclure et cocher “Mirror” pour avoir les animations “tourner à gauche”




A faire

  • Sur le même principe, améliorez votre contrôleur pour qu'il prenne en compte les sauts et les déplacements accroupis.



A.4. Cinématique inverse pour placer les pieds sur le sol


Pour placer les pieds sur un sol non plat, nous allons regardez les fonctionnalités d'Unity pour faire de la cinématique inverse.

  • Ajoutez un plan incliné à votre scène

  • Modifiez votre personnage pour que ses pieds soient bien placés sur le sol tout au long de la marche : l'avant du pied et le talon bien orientés sur le sol à chaque pas.


A.5. Interaction avec l'environnement : la physique

  • Créez deux cubes l'un sur l'autre comme ceci

  • Ajoutez des Component “Rigid Body” à chacun des deux cubes
  • Pour que le personnages interagissent avec les cubes, vous devez ajouter un “Ragdoll” (traduction : poupée désarticulée) à votre personnage. Ce processus va créer un RigidBody pour chaque articulation de votre personnage.
    • Pour cela, bouton droit créez un ragdoll,
    • puis faites un glisser de chaque articulation de votre squelette dans la bonne case du ragdoll.


A.6 Idée : pour aller plus loin

  • (facile) Ajoutez à votre personnage, la possibilité de shooter des sphères.
  • (très facile) Construisez un mur de brique (avec un script, il ne s'agit pas de placer les dizaines de cube à la main ;-)
  • Ajouter un 2e personnage fixe qui regarde toujours dans la direction de votre personnage : tourne la tête, puis éventuellement tourne le corps. Ce personnage déplace sa main vers le projectile que vous lui envoyez : voir par exemple cette vidéo.



Partie B : Cinématique Inverse Personnalisée

L'objectif de cette partie est de réaliser une cinématique inverse personnalisée (custom Inverse Kinematic IK) d'un personnage sous Unity. L'algorithme de cinématique inverse que nous allons coder se base sur FABRIK.

Il existe déjà un algorithme de cinématique inverse dans Unity. Il ne fonctionne que avec un squelette de type humanoid. Dès que votre personnage est différent il faut réécrire un algorithme d'IK. De plus, un des objectif de ce TP serait de comparer leur algorithme avec le votre. Il existe des implémentations de FABRIK dans le store, jouez le jeu de recoder le votre. FABRIK n'est pas un algorithme compliqué.

Il est envisageable d'aller jusqu'à utiliser notre algorithme d'IK pour faire marcher notre personnage en définissant des trajectoire de pieds, ce qui est une approche complétement différente car procédurale de l'approche classique proposer dans la partie MECANIM de ce TP.

B.1 La cible

  • Créez un objet simple que nous allons piloter au clavier et qui sera la cible d'une extrémité (un main ou un pied). Menu “GameObject” puis “3D Object” puis “Sphere”
  • Pour que l'objet fasse quelque chose, il faut écrire du code pour lui dire quoi faire. Dans le panneau Project, sélectionnez Create en haut (ou cliquez avec le bouton droit de la souris n'importe où dans le panneau) et sélectionnez C# script. Nommez-le IKTargetMovement.
  • Double-cliquez sur votre script pour l'ouvrir (Visual Studio se lance). Chaque fois que vous créez un nouveau script, Unity ajoute deux méthodes par défaut, Start() et Update(). 'Start' est appelé une fois, lorsque l'objet entre dans le monde du jeu. La mise à jour 'Update' est appelée à chaque image/rendu.
  • Copiez et collez la ligne suivante dans la méthode de mise à jour (Update).
   if (Input.GetKey(KeyCode.LeftArrow))
      transform.position += Vector3.left;
  • Sélectionnez votre sphere en cliquant dessus. A droite dans le panel “Inspector”, Ajoutez un composant “Add component”, puis trouvez votre script de mouvement dans la petite barre de recherche (ou section “script”) et ajoutez-le à votre capsule.
  • Appuyez sur le bouton de lecture en haut de l'éditeur.
  • Completez le script avec les 4 (voir 6) directions



B.2 Un squelette simple (1 unique chaine cinématique)

Un squelette est un arbre hiérarchique. Unity représente le monde sous forme d'un arbre (le graphe de scène).

  1. Créez un 1ère sphère de nom A1 qui sera l'“Epaule” du squelette;
  2. Puis créez A2 le Coude, A3 le Poignet et A4 le Doigt.

Votre hiérarchie de scène doit ressembler à ceci.



En sélectionnant la racine de votre objet (A1), créez maintenant un script 'IK.cs' avec le bouton “Add Component”, tapez IK. Comme aucun script de ce nom n'existe Unity vous propose d'en créer un. Vous pouvez partir de ce script à compléter.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class IK : MonoBehaviour
{
    // Le transform (noeud) racine de l'arbre, 
    // le constructeur créera une sphère sur ce point pour en garder une copie visuelle.
    public GameObject rootNode = null;         
    
    // Un transform (noeud) (probablement une feuille) qui devra arriver sur targetNode
    public Transform srcNode = null;
    
    // Le transform (noeud) cible pour srcNode
    public Transform targetNode = null;                         

    // Si vrai, recréer toutes les chaines dans Update
    public bool createChains = true;                            
    
    // Toutes les chaines cinématiques 
    public List<IKChain> chains = new List<IKChain>();          

    // Nombre d'itération de l'algo à chaque appel
    public int nb_ite = 10;                                     


    void Start()
    {
        if (createChains)
        {
            Debug.Log("(Re)Create CHAIN");
            createChains = false;    // la chaîne est créée une seule fois, au début
            
            // TODO : 
            // Création des chaînes : une chaîne cinématique est un chemin entre deux nœuds carrefours.
            // Dans la 1ere question, une unique chaine sera suffisante entre srcNode et rootNode.
            
            // TODO-2 : Pour parcourir tous les transform d'un arbre d'Unity vous pouvez faire une fonction récursive
            // ou utiliser GetComponentInChildren comme ceci :
            // foreach (Transform tr in gameObject.GetComponentsInChildren<Transform>())

            
            // TODO-2 : Dans le cas où il y a plusieurs chaines, fusionne les IKJoint entre chaque articulation.
        }
    }

    void Update()
    {
        if (createChains)
            Start();

        if (Input.GetKeyDown(KeyCode.I))
        {
            IKOneStep(true);
        }
        
        if (Input.GetKeyDown(KeyCode.C))
        {
            Debug.Log("Chains count="+chains.Count);
            foreach (IKChain ch in chains)
                ch.Check();
        }
    }


    void IKOneStep(bool down)
    {
        int j;

        for (j = 0; j < nb_ite; ++j)
        {
            // TODO : IK Backward (remontée), appeler la fonction Backward de IKChain 
            // sur toutes les chaines cinématiques.

            // TODO : appliquer les positions des IKJoint aux transform en appelant ToTransform de IKChain

            // IK Forward (descente), appeler la fonction Forward de IKChain 
            // sur toutes les chaines cinématiques.
                      
            // TODO : appliquer les positions des IKJoint aux transform en appelant ToTransform de IKChain
            
        }



    }


}



La classe IKChain va gérer une unique chaine cinématique. Les articulations seront stockées dans la liste 'joints'.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;




public class IKChain
{
    // Quand la chaine comporte une cible pour la racine. 
    // Ce sera le cas que pour la chaine comportant le root de l'arbre.
    private IKJoint rootTarget = null;                              
    
    // Quand la chaine à une cible à atteindre, 
    // ce ne sera pas forcément le cas pour toutes les chaines.
    private IKJoint endTarget = null;                               

    // Toutes articulations (IKJoint) triées de la racine vers la feuille. N articulations.
    private List<IKJoint> joints = new List<IKJoint>();             
    
    // Contraintes pour chaque articulation : la longueur (à modifier pour 
    // ajouter des contraintes sur les angles). N-1 contraintes.
    private List<float> constraints = new List<float>();            
    
    
    // Un cylndre entre chaque articulation (Joint). N-1 cylindres.
    //private List<GameObject> cylinders = new List<GameObject>();    



    // Créer la chaine d'IK en partant du noeud endNode et en remontant jusqu'au noeud plus haut, ou jusqu'à la racine
    public IKChain(Transform _rootNode, Transform _endNode, Transform _rootTarget, Transform _endTarget)
    {
        Debug.Log("=== IKChain::createChain: ===");
        // TODO : construire la chaine allant de _endNode vers _rootTarget en remontant dans l'arbre (ou l'inverse en descente). 
        // Chaque Transform dans Unity a accès à son parent 'tr.parent'
    }


    public void Merge(IKJoint j)
    {
        // TODO-2 : fusionne les noeuds carrefour quand il y a plusieurs chaines cinématiques
        // Dans le cas d'une unique chaine, ne rien faire pour l'instant.
    }


    public IKJoint First()
    {
        return joints[0];
    }
    public IKJoint Last()
    {
        return joints[ joints.Count-1 ];
    }

    public void Backward()
    {
        // TODO : une passe remontée de FABRIK. Placer le noeud N-1 sur la cible, 
        // puis on remonte du noeud N-2 au noeud 0 de la liste 
        // en résolvant les contrainte avec la fonction Solve de IKJoint.
    }

    public void Forward()
    {
        // TODO : une passe descendante de FABRIK. Placer le noeud 0 sur son origine puis on descend.
        // Codez et deboguez déjà Backward avant d'écrire celle-ci.
    }

    public void ToTransform()
    {
        // TODO : pour tous les noeuds de la liste appliquer la position au transform : voir ToTransform de IKJoint
    }

    public void Check()
    {
        // TODO : des Debug.Log pour afficher le contenu de la chaine (ne sert que pour le debug)
    }

}



La classe IKJoint ca stoker un lien vers les Transform de l'arbre de scène Unity, ainsi que la position du noeud durant l'algorithme de FABRIK. Cette position sera appliquée au Transform par la fonction 'ToTransform'.



using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;

public class IKJoint
{
    // la position modifiée par l'algo : en fait la somme des positions des sous-branches. 
    // _weight comptera le nombre de sous-branches ayant touchées cette articulation.
    private Vector3 _position;      
    
    // un lien vers le Transform de l'arbre d'Unity
    private Transform _transform;
    
    // un poids qui indique combien de fois ce point a été bougé par l'algo.
    private float _weight = 0.0f;   


    public string name
    {
        get
        {
            return _transform.name;
        }
    }
    public Vector3 position     // la position moyenne
    {
        get
        {
            if (_weight == 0.0f) return _position;
            else return _position / _weight;
        }
    }

    public Vector3 positionTransform
    {
        get
        {
            return _transform.position;
        }
    }

    public Transform transform
    {
        get
        {
            return _transform;
        }
    }

    public Vector3 positionOrigParent
    {
        get
        {
            return _transform.parent.position;
        }
    }

    public IKJoint(Transform t)
    {
        // TODO : initialise _position, _weight
    }

    public void SetPosition(Vector3 p)
    {
        // TODO
    }

    public void AddPosition(Vector3 p)
    {
        // TODO : ajoute une position à 'position' et incrémente '_weight'
    }


    public void ToTransform()
    {
        // TODO : applique la _position moyenne au transform, et remet le poids à 0
    }

    public void Solve(IKJoint anchor, float l)
    {
        // TODO : ajoute une position (avec AddPosition) qui repositionne _position à la distance l
        // en restant sur l'axe entre la position et la position de anchor
    }
}



B.3 Un squelette à plusieurs branches (plusieurs chaines cinématiques)


  1. Commencez par créer un squelette avec plusieurs chaines cinématique comme indiqué sur la figure ci-dessous;
  2. Modifiez le code précédent pour gérer plusieurs branches dans un arbre.




B.4 Gestion des angles


Chargez un squelette humain classique (voir la partie B. de ce TP). Pour l'instant nous avons simplement déplacé les positions des articulations dans la fonction 'Solve'. Pour bien faire, il faudrait plutôt modifier également l'orientation de l'articulation.



B.5 Comparaison avec l'IK d'Unity


Unity implémente déjà un algorithme d'IK sur les personnages humanoïdes. Voir Section A.3 de cette page. Placez deux squelettes humanoïdes l'un à coté de l'autre et comparer les deux approches d'IK.








Partie C : Edition multi-résolution d'une animation

L'objectif de cette partie est de transformer une animation par une édition multi-résolutions. Un peu comme les slider d'un equalizer permettent de changer le son des aigus aux graves : amplifier les grandes amplitudes pour exagérer un mouvement sans toucher aux petites amplitudes pour ne pas ajouter des tremblements (ou l'inverse). Tout est très bien expliqué dans l'article suivant section 2, en particulier 2.1 :



C.1. Trace 3D de chaque articulations

L'objectif de cette partie est d'afficher une animation en traçant la courbe suivie par chaque articulation. Cela servira une fois la multi résolution calculée pour observer l'effet sur les courbes de trajectoires. Nous allons créer et ajouter un nouveau type d'“inspector” que l'on pourra nommer “Play Animation”. Cette partie de l'interface permettra de choisir l'animation à afficher et offrira des cases à cocher pour chaque articulations indiquant si la trajectoire doit être affichée ou non.


  • Créez un folder “Editor” qui contiendra le script PlayAnimationEditor.cs
  • Rangez également tous les autres fichiers comme ceci

  • Le squelette vide de PlayAnimationEditor.cs est plus bas
  • Ouvrir la fenêtre avec le menu Window/PlayAnimationEditor
  • Quand vous modifier le script et repasser sous Unity, il y a une petite roue qui tourne en bas à droite indiquant que votre script compile, puis la ligne du bas indique la 1ere erreur

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEditor;

public class PlayAnimationEditor : EditorWindow
{
    // Current GameObject Selected on the Scene
    [SerializeField]
    protected GameObject m_skeleton;

    // List of all body joints of the current Skeleton
    // Transform -> Unity Type (Position, Rotation, Scale)
    // Matrix Transform
    [SerializeField]
    public List<Transform> m_BodyJoints;

    // Current AnimationClip Selected by the user
    // This AnimationClip will be played 
    [SerializeField]
    protected AnimationClip m_animationClip;


    // Boolean to check if the animation is playing or not
    private bool m_b_isRunning = false;

    // Stocks the current Time
    protected float m_f_time = 0.0f;

    // Vector2 used to get the save the position of the scroll in the Window
    Vector2 m_scrollPostion = Vector2.zero;

    // First frame in seconds (here it will be 0.0f)
    private float m_f_startTime = 0.0f;
    // Length of the AnimationClip in seconds 
    private float m_f_endTime;
    // Frame Duration in seconds
    private float m_f_frameDuration;
    // accelerate or slow the animation
    protected float m_f_scaleTime = 1f;


    // Dictionnary of string & List<Vector3> which contains all the position of one body Joint
    private Dictionary<string, List<Vector3>> m_trajectories;

    // Dictionnary of string & bool which contains all the bool used by the toggle button
    protected Dictionary<string, bool> m_toggleTraj;



    // The new item in the menu
    [MenuItem("Window/PlayAnimation", false, 2000)]
    public static void DoWindow()
    {
        PlayAnimationEditor m_window = GetWindow<PlayAnimationEditor>();       
    }

    // Called when the user change the selected object in the scene windows
    public void OnSelectionChange()
    {
        // Pick the current selected gameObject in the Scene.
        m_skeleton = Selection.activeGameObject;
        // The user can pick in the void
        if (m_skeleton != null)
        {
            // Check if the current gameObject has an animator component or an animation            
            if (m_skeleton.GetComponent<Animation>() || m_skeleton.GetComponent<Animator>())
            {
                if (m_BodyJoints == null)
                    m_BodyJoints = new List<Transform>();

                // Get all the bodyJoints -> This is specific to the skeleton that i used
                // One body joint is defined when 
                m_BodyJoints = m_skeleton.GetComponentsInChildren<Transform>().Where(x => x.childCount != 0).ToList();
                //Repaint the currentWindow -> Call the OnGui function
                Repaint();
            }
        }      
    }



    // This function enables the editor to handle an event in the scene view.
    // We need to redraw the curve & points when the user is interacting with the SceneView
    // In this function, you will have to code the drawing of the trajectories
    public void OnSceneGUI(SceneView sceneView)
    {
        // TODO
        // Parcourir toutes les articulations
        //    Si la togglebox est cochée
        //       Parcourir tous les points de la trajectoire et les afficher
        // Vous pourrez appeler la fonction de dessin d'un point et/ou Handles.DrawLine(l_oldPoint, l_currentPoint);
    }


    // Init the trajectories for each body joints for the current Animation Clip
    private void initTrajectories()
    {
        // Enable the Animation Mode if disabled
        if (!AnimationMode.InAnimationMode()) AnimationMode.StartAnimationMode();


        // TODO
        // Créer ou vider dictionnaire m_trajectories qui va contenir la list des points de la trajectoire
        // Créer ou vider dictionnaire m_toggleTraj qui va contenir la list des bool indiquant si la trajectoire est visible ou non
        // Remplir le m_trajectories[m_BodyJoints[i].name] avec les positions des points
        // Voir AnimationMode.BeginSampling(); .... AnimationMode.EndSampling(); qui se trouve dans playAnimation


    }


    // OnGUI is called for rendering and handling GUI events.
    // Use OnGUI to draw all the controls of your window.
    public void OnGUI()
    {
        // We need to select a GameObject in the Scene
        if (m_skeleton == null)
        {
            EditorGUILayout.HelpBox("Please select a GameObject.", MessageType.Info);
            return;
        }
        // Check if the current GameObject is active
        if (!m_skeleton.active)
        {
            EditorGUILayout.HelpBox("Please select a GameObject that is active.", MessageType.Info);
            return;
        }
        // Check if the current GameObject has an Animator or Animation Component
        if (m_skeleton.GetComponent<Animator>() == null && m_skeleton.GetComponent<Animation>() == null)
        {
            EditorGUILayout.HelpBox("Please select a GameObject with an Animator Component or Animation.", MessageType.Info);
            return;
        }

        // Update the scroll Position
        m_scrollPostion = GUILayout.BeginScrollView(m_scrollPostion, false, false);

        // Begin a vertical group that will contains all the gui element we will declare between the BeginVertical and the EndVertical
        EditorGUILayout.BeginVertical();

        // Create a "Listener" on the next GUI Element in order to track some change on it
        EditorGUI.BeginChangeCheck();
        // Create an Object Field of type AnimationClip
        // This allows the user to select which AnimationClip he wants to play
        m_animationClip = EditorGUILayout.ObjectField("Current Animation Clip", m_animationClip, typeof(AnimationClip), false) as AnimationClip;
        // If the animation clip has changed, we need to compute the new Trajectories !
        if (EditorGUI.EndChangeCheck())
        {
            initTrajectories();
        }

        // If the user has selected an AnimationClip
        if (m_animationClip != null)
        {
            // Get the Length of the current Animation
            m_f_endTime = m_animationClip.length;
            // Get the frame Duration of the current Animation
            m_f_frameDuration = 1.0f / m_animationClip.frameRate;
            // An example for a Slider with change detect
            // So we need to call The  EditorGUI.BeginChangeCheck() in order to create a "Listener"
            EditorGUI.BeginChangeCheck();
            // Then we create the Object that we want to track some change on 
            m_f_time = EditorGUILayout.Slider("Time (seconds)", m_f_time, m_f_startTime, m_f_endTime);
            // If the user has modified the Slider Precision here, we can detect it and call a fonction for example
            if (EditorGUI.EndChangeCheck())
                samplePosture(m_f_time);

            EditorGUI.BeginChangeCheck();
            // Then we create the Object that we want to track some change on 
            m_f_scaleTime = EditorGUILayout.Slider("Scale Time", m_f_scaleTime, 0.0f, 2.0f);

            if (!m_b_isRunning)
            {
                // Create a Button in order to plays the AnimationClip
                if (GUILayout.Button("Start Animation"))
                {
                    // Starts the Coroutine that will play the Animation
                    //Swing.Editor.EditorCoroutine.start(repeatAnimation(m_f_frameDuration));
                    // Coroutine is runnning
                    m_b_isRunning = true;
                }
            }
            else
            {
                // Stop the Coroutine
                if (GUILayout.Button("Stop Animation"))
                {
                   // Swing.Editor.EditorCoroutine.stop(repeatAnimation(m_f_frameDuration));
                    m_b_isRunning = false;
                }
            }


            // TODO IHM : 
            // faire un for sur les articulations, creer des boites a cocher pour chaque articulation
            // Voir la doc : https://docs.unity3d.com/ScriptReference/EditorGUILayout.Toggle.html
            // Utilisez la variables : m_toggleTraj[m_BodyJoints[i].name]


        }

        // End the vertical group
        EditorGUILayout.EndVertical();

        // Stop the Scroll
        GUILayout.EndScrollView();
    }


    // Call at each frame
    // In this function, we will play the Animation
    private void Update()
    {
        // TODO
        // Verifier que m_skeleton m_animationClip, m_b_isRunning sont init
        // modifier le temps : m_f_time
        // appeler samplePosture qui est ue fonction un peu plus bas



        SceneView.RepaintAll();
    }


    // Sample our Skeleton at the time given in parameter for the currentAnimationClip
    private void samplePosture(float p_f_time)
    {
        // Check if the Game isn't running & the Animation Mode is enabled
        if (!EditorApplication.isPlaying && AnimationMode.InAnimationMode())
        {
            // We need to BeginSampling before the SampleAnimationClip is called
            AnimationMode.BeginSampling();
            // Samples the animationClip (m_animation) at the time (m_f_time) for the skeleton (m_skeleton)
            // If the GameObject & the AnimationClip are different -> no errors are trigger but nothing happen 
            AnimationMode.SampleAnimationClip(m_skeleton, m_animationClip, p_f_time);
            // Ending the Sampling of the Animation
            AnimationMode.EndSampling();
            // Repaint The SceneView as the skeleton has changed
            SceneView.RepaintAll();
            // Repaint the GUI as we are changing the variable m_f_time on which we have a slider
            Repaint();
        }
    }

    void OnFocus()
    {
        // Remove delegate listener if it has previously
        // been assigned.
        SceneView.onSceneGUIDelegate -= this.OnSceneGUI;
        // Add (or re-add) the delegate.
        SceneView.onSceneGUIDelegate += this.OnSceneGUI;
    }

    void OnDestroy()
    {
        // When the window is destroyed, remove the delegate
        // so that it will no longer do any drawing.
        SceneView.onSceneGUIDelegate -= this.OnSceneGUI;
    }
}




C.2. Les filtres de trajectoire

  • Ajouter un bouton “Gaussienne” qui passe un filtre gaussien sur un clip. Dans la fonction OnGuit() ajoutez
     if (GUILayout.Button("Gaussian filter"))
     {
        GaussianAnim();
     }
  • puis la fonction
    public void GaussianAnim()
    {
        AnimationClip clip = new AnimationClip();  // comportera la copie de m_animationClip mais filtrer

        // Copy the m_animationClip in the local variale clip
        clip.legacy = m_animationClip.legacy;
        foreach (EditorCurveBinding binding in AnimationUtility.GetCurveBindings(m_animationClip))
        {
            AnimationCurve curve = AnimationUtility.GetEditorCurve(m_animationClip, binding);
            
            //TODO : editer chaque courbe ici avec
            // Parcourir toute la courbe avec comme longueur : curve.length 
            //  float v = curve.keys[time].value;
            //  curve.MoveKey(time, new Keyframe(time, v));

            
            AnimationUtility.SetEditorCurve(clip, binding, curve);
        }

        // Si vous avez besoin de (quaternion+translation) il faut les regrouper les courbes
        // il y a 7 courbes par articulations, par exemple pour le noeud "root" il y a 
        // "rootT.x"  "rootT.y" "rootT.z" pour la translation
        // "rootQ.x"  "rootQ.y" "rootQ.z" "rootQ.w" pour le quaternion
        // Il faut donc regrouper ces 4 courbes en un tableau de quaternion

        // Save the local variale clip
        AssetDatabase.CreateAsset(clip, "Assets/Gaussian.anim");
    }



Un fois le filtre bien maitrisé, passez à l'aspect multi-résolution.

  • Pour chaque rotation de chaque articulation, construire la représentation multi-résolution. Une animation d'une articulation se reconstruit en partant de la valeur moyenne Gn+la somme de toutes les variations aux différentes résolutions.

  • La ligne tout en haut est la courbe originale, les lignes d'en dessous sont des moyennes et des déplacements (=des écarts à la moyenne, valeurs rouges). Pour passer d'une ligne à la ligne d'en dessous, on moyenne deux par deux les valeurs et on obtient : la moyenne et le déplacement. Pour revenir à la courbe d'animation on remonte en additionnant à la moyenne le déplacement. Les sliders sont des coefficients multiplicateurs qu'on applique aux déplacements (valeurs rouges), ce qui change le signal quand on remonte.
  • Par exemple avec une courbe ayant 2 valeurs. Courbe = 10, 14
  • On obtient (en descendant) : Moyenne = 12; déplacement=-2, +2 (car 12-2=10 et 12+2=14)
  • Si on multiplie les déplacements par 1.5 on obtient : Moyenne = 12; déplacement=-2*1.5=-3, +2*1.5=3
  • En reconstruisant la courbe (remontée) on obtient : Courbe = 12-3=9, 12+3=15
  • On a gardé une courbe centré en 12 mais ayant une amplitude plus grande.
  • Maintenant avec une courbe de 256 valeurs. On peut descendre 8 fois, car la courbe peut être moyennée par 2, 8 fois. Cela donne 8 sliders.
  • Demander à l'utilisateur via l'interface d'Unity les n (n=8 pour une animation à 256 valeurs) coefficients, multiplier chaque variation par ce coefficient puis reconstruire le signal.




Annexe : Faire du debug avec Unity et Visual Studio

  • Unity vous affiche en rouge en bas des actions non valide
  • Pour afficher du texte et une variable dans la console
Debug.Log("vSpeed = " + Input.GetAxis("Vertical"));


  • Vous pouvez attacher Unity à Visual Studio et mettre des points d'arrêt. Pour cela, dans la barre des bouton vous cliquez sur “Attacher à Unity” puis le triangle de run.