Le LSL de Second Life

Ce site est consacré principalement au langage LSL de Second Life...

« Retour aux projets


Animation de primitives à partir d'un fichier BVH


Génération du squelette

Un fichier bvh est constitué de deux parties :

Pour générer le squelette nous avons donc juste besoin de la partie HIERARCHY. Voici celle qui m'a servi de point de départ (que j'ai téléchargée dans ce pack de Second Life : fichier avatar_stand_1.bvh):


C'est une structure hiérarchique bien organisée. Chaque élément est défini avec un offset de position par rapport au précédent. Générer un squelette à partir de ces informations consiste donc juste à lire ces données et à créer l'os qui correspond à chaque étape en le positionnant avec l'offset.

Il nous faut maintenant un script pour lire ces données et générer les os :

/////////////////////////////////////////////////////////////
//                                                         //
//                    Builder V 1.0                        //
//                                                         //
//   Génération de primitives à partir d'un fichier bvh    //
//                                                         //
//                   Bestmomo Lagan                        //
//                                                         //
/////////////////////////////////////////////////////////////

// Paramètres
float RAPPORT = .04;
vector BASE = <0, 0, 2.5>;
integer CHANNEL = -565684510;
string NOTE = "bvh";

// Variables
integer i_notecardLine;
key k_query;
string s_nom;
list l_pile;

// Génération de l'offset
vector get_offset(string data) {
    data = llGetSubString(data, 7, -1);
    list l = llParseString2List(data, [" "], []);
    return <(float)llList2String(l, 2), (float)llList2String(l, 0), (float)llList2String(l, 1)>;
}

// Génération os
generate(vector v_offset) {
    // Position de départ
    vector v_pos_start = llList2Vector(l_pile, llGetListLength(l_pile) - 1);
    // Position finale
    vector v_pos_end = v_pos_start + v_offset * RAPPORT;
    // Ajout dans la pile
    l_pile += v_pos_end;
    // Paramètres pour l'os
    vector v_pos_bone = v_pos_start + (v_pos_end - v_pos_start) / 2.0;
    vector v_dir = llVecNorm(v_pos_end - v_pos_start);
    rotation r_rot = llRotBetween(<0,0,1>, v_dir);
    float f_longueur = llVecDist(v_pos_start, v_pos_end) * .9;
    // Génération de l'os
    llRezObject("os", llGetPos() + v_pos_bone, ZERO_VECTOR, r_rot, CHANNEL);
    // Message à l'os
    llWhisper(CHANNEL, (string)f_longueur + "@" + s_nom);
}

default
{
    touch_start(integer total_number) {
        if(llGetInventoryType(NOTE) == INVENTORY_NOTECARD) {
            // Message
            llOwnerSay("Génération en cours, un peu de patience...");
            // Initialisation pile
            l_pile = [BASE];
            // Mise en place bassin
            llRezObject("boule", llGetPos() + BASE, ZERO_VECTOR, ZERO_ROTATION, 0);
            // Initialisation de la note
            i_notecardLine = 5;
            k_query = llGetNotecardLine(NOTE, i_notecardLine++);
        }
    }

    dataserver(key id, string data)    {
        if(k_query == id) {
            //llOwnerSay(data);
            // Elimination des espaces
            data = llStringTrim(data, STRING_TRIM);
            // Nouveau joint
            if(llGetSubString(data, 0, 4) == "JOINT") {
                // Nom du joint
                s_nom = llGetSubString(data, 6, -1);
            }
            // Fin de segment
            else if(llGetSubString(data, 0, 7) == "End Site") {
                // Nom du dernier os du segment
                s_nom = "end_" + s_nom;
            }
            // Offset
            else if(llGetSubString(data, 0, 5) == "OFFSET") {
                // Génération de l'os
                generate(get_offset(data));
            }
            // Fin de section
            else if(llGetSubString(data, 0, 0) == "}") {
                // Dépilage
                l_pile = llDeleteSubList(l_pile, -1, -1);
                // Fin de la hiérarchie
                if(!llGetListLength(l_pile)) {
                    llOwnerSay("Génération terminée !");
                    return;
                }
            }
        }
        k_query = llGetNotecardLine(NOTE, i_notecardLine++);
    }
}  
		

Avec ces paramètres :

Vous mettez donc ce script dans une boite posée au sol. Il faut mettre aussi la note bvh qui contient les données.

Pour que ça fonctionne vous devez aussi créer un os, j'ai utilisé un simple cylindre allongé sur son axe Z (X = 0.05, Y = .05, Z = .15). Vous mettez ce script dans l'os :

// Paramètres
integer CHANNEL = -565684510;

default
{
    on_rez(integer i) {
        // Ecoute du canal
        llListen(CHANNEL, "", NULL_KEY, "");
    }

    listen(integer channel, string name, key id, string message) {
        list l = llParseString2List(message, ["@"], []);
        // Dimension
        vector v_scale = llGetScale();
        llSetScale(<v_scale.x, v_scale.y, (float)llList2String(l, 0)>);
        // Nom
        llSetObjectName(llList2String(l, 1));
        // Destruction du script
        llRemoveInventory(llGetScriptName());        
    }
}  
		

Et vous mettez l'os dans le rezzer en le nommant os.

Créez aussi une petite sphère (0.1, 0.1, 0.1) nommée boule que vous mettez aussi dans le rezzer. Au final dans ce rezzer vous devez avoir :

Il ne vous reste plus qu'à cliquer sur le rezzer pour que la génération se fasse .

Quand le squelette est généré vous pouvez lier tous les os en faisant en sorte que la boule soit le root. Chaque os porte le nom qui est prévu pour l'articulation dans la hiérarchie, ce qui permet de les identifier. Votre squelette est maintenant prêt pour être animé, mais ça c'est une autre histoire que nous allons voir maintenant...


Animation du squelette

Pour animer ce squelette nous allons utiliser 3 fichiers bvh faisant évidemment partie du pack de Second Life :

Pour que la section MOTION soit lisible dans SL il faut la compacter. Pour simplifier cette procédure j'ai créé un utilitaire en ligne.

Il faut préparer une note par animation et les nommer de façon explicite. Par exemple marche pour la marche et ajouter au nom ".bvh". Donc par exemple la note pour la marche se nommera marche.bvh. Il faut préparer les 3 notes et les mettre dans l'inventaire du squelette. Pour vous simplifier la vie je vous mets directement le contenu des 3 notes ci-après :

Debout.bvh (issu de avatar_stand_1)


Clavier.bvh (issu de avatar_type)


Marche.bvh (issu de avatar_walk)


On met dans le squelette les 3 notes. On prépare dans son inventaire (celui de votre avatar, pas le squelette ), 3 scripts nommés Debout, Marche et Clavier avec ce script :

/////////////////////////////////////////////////////////////
//                                                         //
//       Animation squelette à partir de fichier bvh       //
//                                                         //
//              Module d'animation V 2.03                  //
//                                                         //
//                   Bestmomo Lagan                        //
//                                                         //
/////////////////////////////////////////////////////////////

// Paramètres
float RAPPORT = .04;

// Variables
string s_note;
integer iNotecardLine;
key kQuery;
list l_rot_type;
integer i_rot_type;
integer i_flag = FALSE;
list l_offset;
list l_positions;
list l_rotations;
list l_rots_base;
list l_num_prims;
list l_sequence;
list l_pos;
list l_rot;
integer i_niveau;
integer step;
integer step_max;
vector v_pos_ref;
list l_params;
string s_last_nom;
integer i_rot_nbr;
rotation r;
integer i_stop;
integer i_move;
integer i_first;

// Calcul offset
vector get_offset(string data) {
    data = llStringTrim(llGetSubString(data, llSubStringIndex(data, "OFFSET") + 7, -1), STRING_TRIM);
    list l = llParseString2List(data, [" "], []);
    return <(float)llList2String(l, 2), (float)llList2String(l, 0), (float)llList2String(l, 1)> * RAPPORT;
}

// Numéro d'un prim
set_num_prim(string nom) {
    integer n = llGetNumberOfPrims();
    while(llList2String(llGetObjectDetails(llGetLinkKey(n), [OBJECT_NAME]), 0) != nom) --n;
    l_num_prims += n;
}

// Routine d'animation
anim_os(integer n, integer i) {
    vector v_start = llList2Vector(l_pos, n - 1);
    vector v_end = v_start + llList2Vector(l_offset, i) * r;
    if(llGetListLength(l_params)) l_params += [PRIM_LINK_TARGET, llList2Integer(l_num_prims, i)];
    else if (i_first == -1) i_first = llList2Integer(l_num_prims, i);
    l_params += [
        PRIM_POS_LOCAL, v_start + (v_end - v_start) / 2.0,
        PRIM_ROT_LOCAL, llList2Rot(l_rots_base, i) * r
            ];
    l_pos += v_end;
    l_rot += r;
}

// Animation
anim(integer nbr) {
    vector v_pos = llGetPos();
    rotation r_rot = llGetRot();
    // Offset éventuel du root
    vector v_first_now = llList2Vector(llGetLinkPrimitiveParams(llList2Integer(l_num_prims, 0), [PRIM_POS_LOCAL]), 0);
    vector v_first_start = llList2Vector(l_offset, 0);
    vector f_root_offset = v_first_now - v_first_start;
    // Boucle générale
    while(nbr--) {
        step = 0;
        i_first = -1;
        for(; step < step_max; ++step) {
            l_params = [];
            // Root
            vector v_root = v_pos + llList2Vector(l_positions, step * i_rot_nbr) * r_rot;
            rotation r_root = llList2Rot(l_rotations, step * i_rot_nbr) * r_rot;
            if(i_move) {
                i_first = LINK_ROOT;
                l_params += [PRIM_POSITION, v_root, PRIM_ROTATION, r_root];
            }
            // Autres éléments
            integer i;
            integer j;
            for(;i < i_rot_nbr; ++i) {
                // Elément fixé au root
                if(!llList2Integer(l_sequence, i)) {
                    l_pos = [llList2Vector(l_offset, i + j) + f_root_offset];
                    l_rot = [ZERO_ROTATION];
                    ++j;
                }
                // Traitement récursion
                else if(llList2Integer(l_sequence, i) != llGetListLength(l_pos) - 1) {
                    integer i_start = llList2Integer(l_sequence, i);
                    l_pos = llDeleteSubList(l_pos, i_start, -1);
                    l_rot = llDeleteSubList(l_rot, i_start, -1);
                    integer m = llGetListLength(l_pos);
                    r = llList2Rot(l_rot, m - 1);
                    anim_os(m, i + j);
                    ++j;
                }
                // Traitement enchaînement
                integer n = llGetListLength(l_pos);
                r = llList2Rot(l_rotations, step * i_rot_nbr + i + 1) * llList2Rot(l_rot, n - 1);
                anim_os(n, i + j);
            }
            llSetLinkPrimitiveParamsFast(i_first, l_params);
        }
        if(i_move) llSetLinkPrimitiveParamsFast(LINK_ROOT, [PRIM_POSITION, v_pos, PRIM_ROTATION, r_rot]);
    }
}

default
{
    state_entry()
    {
        s_note = llGetScriptName() + ".bvh";
        // Dans MOTION chaque ligne a n références de 3 valeurs : 1 position du root et n - 1 rotations (dont la première est le root)
        if(llGetInventoryType(s_note) == INVENTORY_NOTECARD) {
            llOwnerSay("Lecture note " + s_note + " en cours, un peu de patience...");
            iNotecardLine = 5;
            //l_sequence = [0];
            i_niveau = -1;
            kQuery = llGetNotecardLine(s_note, iNotecardLine++);
        }
    }

    dataserver(key id, string data) {
        if(kQuery == id) {
            if (data != EOF) {
                if(step == 0) {
                    if(~llSubStringIndex(data, "Frames:")) {
                        step_max = (integer)llGetSubString(data, 7, -1);
                        ++iNotecardLine;
                        ++step;
                    }
                    else if(~llSubStringIndex(data, "OFFSET")) l_offset += get_offset(data);
                    else if(~llSubStringIndex(data, "CHANNELS")) {
                        if(~llSubStringIndex(data, "Xrotation Zrotation Y")) l_rot_type += 0;
                        else if(~llSubStringIndex(data, "Xrotation Yrotation Z")) l_rot_type += 1;
                        else if(~llSubStringIndex(data, "Yrotation Zrotation X")) l_rot_type += 2;
                        else if(~llSubStringIndex(data, "Yrotation Xrotation Z")) l_rot_type += 3;
                        else if(~llSubStringIndex(data, "Zrotation Yrotation X")) l_rot_type += 4;
                        else if(~llSubStringIndex(data, "Zrotation Xrotation Y")) l_rot_type += 5;
                        l_sequence += i_niveau;
                    }
                    else if(~llSubStringIndex(data, "JOINT")) {
                        s_last_nom = llStringTrim(llGetSubString(data, llSubStringIndex(data, "JOINT") + 6, -1), STRING_TRIM);
                        set_num_prim(s_last_nom);
                    }
                    else if(~llSubStringIndex(data, "End Site"))
                        set_num_prim("end_" + s_last_nom);
                    else if(~llSubStringIndex(data, "{")) ++i_niveau;
                    else if(~llSubStringIndex(data, "}")) --i_niveau;
                }
                else if(step == 1) {
                    i_flag = !i_flag;
                    list l = llParseString2List(data, [" "], []);
                    // Récupération des positions (1 par ligne)
                    if(i_flag) {
                        vector v = <(float)llList2String(l, 2), (float)llList2String(l, 0), (float)llList2String(l, 1)> * RAPPORT;
                        if(!llGetListLength(l_positions)) v_pos_ref = v;
                        l_positions += v - v_pos_ref;
                    }
                    // Récupération des rotations (21 sur 2 lignes, la première pour le root)
                    integer n = llGetListLength(l);
                    integer i;
                    if(i_flag) {
                        i = 3;
                        i_rot_type = 0;
                    }
                    for(; i < n; i += 3) {
                        if(i_flag && i == 3)
                            l_rotations += llEuler2Rot(<(float)llList2String(l, 4), (float)llList2String(l, 3), (float)llList2String(l, 5)> * DEG_TO_RAD);
                        else {
                            integer j = llList2Integer(l_rot_type, i_rot_type);
                            // Rotation XZY bvh et YXZ sl
                            if(j == 0)
                                l_rotations += llEuler2Rot(<0, 0, (float)llList2String(l, i + 2)> * DEG_TO_RAD)
                                    * llEuler2Rot(<(float)llList2String(l, i + 1), 0, 0> * DEG_TO_RAD)
                                        * llEuler2Rot(<0, (float)llList2String(l, i), 0> * DEG_TO_RAD);
                            // Rotation XYZ bvh et YZX sl
                            else if(j == 1)
                                l_rotations += llEuler2Rot(<(float)llList2String(l, i + 2), 0, 0> * DEG_TO_RAD)
                                    * llEuler2Rot(<0, 0, (float)llList2String(l, i + 1)> * DEG_TO_RAD)
                                        * llEuler2Rot(<0, (float)llList2String(l, i), 0> * DEG_TO_RAD);
                            // Rotation YZX bvh et ZXY sl
                            else if(j == 2)
                                l_rotations += llEuler2Rot(<0, (float)llList2String(l, i + 2), 0> * DEG_TO_RAD)
                                    * llEuler2Rot(<(float)llList2String(l, i + 1), 0, 0> * DEG_TO_RAD)
                                        * llEuler2Rot(<0, 0, (float)llList2String(l, i)> * DEG_TO_RAD);
                            // Rotation YXZ bvh et ZYX sl
                            else if(j == 3)
                                l_rotations += llEuler2Rot(<(float)llList2String(l, i + 2), 0, 0> * DEG_TO_RAD)
                                    * llEuler2Rot(<0, (float)llList2String(l, i + 1), 0> * DEG_TO_RAD)
                                        * llEuler2Rot(<0, 0, (float)llList2String(l, i)> * DEG_TO_RAD);
                            // Rotation ZYX bvh et XZY sl
                            else if(j == 4)
                                l_rotations += llEuler2Rot(<0, (float)llList2String(l, i + 2), 0> * DEG_TO_RAD)
                                    * llEuler2Rot(<0, 0, (float)llList2String(l, i + 1)> * DEG_TO_RAD)
                                        * llEuler2Rot(<(float)llList2String(l, i), 0, 0> * DEG_TO_RAD);
                            // Rotation ZXY bvh et XYZ sl
                            else if(j == 5)
                                l_rotations += llEuler2Rot(<(float)llList2String(l, i), (float)llList2String(l, i + 1), (float)llList2String(l, i + 2)> * DEG_TO_RAD);
                            i_rot_type++;
                        }
                    }
                }
                kQuery = llGetNotecardLine(s_note, iNotecardLine++);
            }
            else {
                llOwnerSay("Note " + s_note + " lue.");
                i_rot_nbr = llGetListLength(l_rot_type) + 1;
                llOwnerSay("Mémorisation des rotations de base...");
                integer i;
                integer n = llGetListLength(l_num_prims);
                l_rots_base = [];
                for(i = 0; i < n; ++i) {
                    list l = llGetLinkPrimitiveParams(llList2Integer(l_num_prims, i), [PRIM_ROT_LOCAL, PRIM_POS_LOCAL]);
                    l_rots_base += llList2Rot(l, 0);
                }
                state animation;
            }
        }
    }
}

state animation
{
    state_entry() {
        llOwnerSay("Animations " + s_note + " prêtes");
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(message == s_note) {
            // Fin d'animation
            if(number == 100) i_stop = TRUE;
            // Lancement animation infinie
            else if(number == 300) {
                i_move = TRUE;
                i_stop = FALSE;
                llSetTimerEvent(.01);
            }
            // Lancement animation infinie sans positionnement
            else if(number == 400) {
                i_move = FALSE;
                i_stop = FALSE;
                llSetTimerEvent(.01);
            }
            // Lancement animation avec nombre de cycles (1001, 1002...)
            else if(number < 2000) {
                i_move = TRUE;
                anim(number - 1000);
                // Message fin d'animation
                llMessageLinked(LINK_THIS, 200, "", NULL_KEY);
            }
            // Lancement animation avec nombre de cycles (2001, 2002...) sans positionnement
            else if(number < 3000) {
                i_move = FALSE;
                anim(number - 2000);
                // Message fin d'animation
                llMessageLinked(LINK_THIS, 200, "", NULL_KEY);
            }
        }
    }

    timer() {
        llSetTimerEvent(.0);
        if(!i_stop) {
            anim(1);
            llSetTimerEvent(.01);
        }
        else llMessageLinked(LINK_THIS, 200, "", NULL_KEY);
    }
}    
		

On met ces 3 scripts dans le squelette (où il y a déjà normalement les 3 notes). On attend que les notes soient lues (bon normalement ça marche ) Ensuite on crée un dernier script dans le squelette (peu importe son nom, moi j'ai choisi command) avec ce code :

/////////////////////////////////////////////////////////////
//                                                         //
//       Animation squelette à partir de fichier bvh       //
//                                                         //
//              Module de commande V 1.10                  //
//                                                         //
//                   Bestmomo Lagan                        //
//                                                         //
/////////////////////////////////////////////////////////////

list l_keys;

lance_marche() {
    llListen(0, "", llGetOwner(), "");
    llDeleteCharacter();
    llCreateCharacter([CHARACTER_MAX_ACCEL, .5, CHARACTER_DESIRED_SPEED, 2.0]);
    llWanderWithin(llGetPos(), <10.0, 10.0, 10.0>, [WANDER_PAUSE_AT_WAYPOINTS, TRUE]);
    llMessageLinked(LINK_THIS, 400, "Marche.bvh", NULL_KEY);
}

arret() {
    llExecCharacterCmd(CHARACTER_CMD_SMOOTH_STOP, []);
    llDeleteCharacter();
    llMessageLinked(LINK_THIS, 100, "Marche.bvh", NULL_KEY);
}

default
{
    state_entry() {
        llListen(0, "", llGetOwner(), "");
        llMessageLinked(LINK_THIS, 2001, "Debout.bvh", NULL_KEY);
    }

    on_rez(integer start_param) {
        llResetScript();
    }

    listen(integer channel, string name, key id, string message) {
        if(message == "marche")
            state marche;
        else if(message == "parle")
            state parle;
        else if(message == "cherche")
            state cherche;
    }
}

state cherche
{
    state_entry() {
        lance_marche();
        llSensorRepeat("", "", AGENT_BY_LEGACY_NAME, 20.0, PI, 5.0);
    }

    sensor(integer total_number) {
        key k = llDetectedKey(0);
        if(llListFindList(l_keys, [k]) == -1) {
            l_keys += k;
            llSensorRemove();
            llSetTimerEvent(0.01);
        }
    }

    timer() {
        llSetTimerEvent(4.0);
        list l = llGetObjectDetails(llList2Key(l_keys, llGetListLength(l_keys) - 1), [OBJECT_POS]);
        if(llGetListLength(l)) {
            vector v = llList2Vector(l, 0);
            if (llVecDist(v, llGetPos()) > 5.0) {
                llNavigateTo(v, []);}
            else
                state message;
        }
        else {
            l_keys = llDeleteSubList(l_keys, -1, -1);
            lance_marche();
            llSensorRepeat("", "", AGENT_BY_LEGACY_NAME, 20.0, PI, 5.0);
        }
    }

    listen(integer channel, string name, key id, string message) {
        if(message == "stop") {
            llSetTimerEvent(.0);
            llSensorRemove();
            arret();
        }
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(number == 200)
            state default;
    }

    state_exit() {
        llSetTimerEvent(.0);
    }
}

state message
{
    state_entry() {
        llMessageLinked(LINK_THIS, 100, "Marche.bvh", NULL_KEY);
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(number == 200) {
            llSay(0, "Hello " + llKey2Name(llList2Key(l_keys, llGetListLength(l_keys) - 1)) + " ! Nice to meet you, my name is Gaston.");
            state clavier;
        }
    }
}

state clavier
{
    state_entry() {
        llDeleteCharacter();
        llMessageLinked(LINK_THIS, 2004, "Clavier.bvh", NULL_KEY);
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(number == 200) {
            state cherche;
        }
    }
}

state marche
{
    state_entry() {
        lance_marche();
    }

    listen(integer channel, string name, key id, string message) {
        if(message == "stop") arret();
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(number == 200)
            state default;
    }
}

state parle
{
    state_entry() {
        llMessageLinked(LINK_THIS, 2004, "Clavier.bvh", NULL_KEY);
        llWhisper(0, "Quelle belle journée !");
    }

    link_message(integer sender_number, integer number, string message, key id) {
        if(number == 200)
            state default;
    }
}    
		

Le squelette doit se mettre en position debout, tranquille. Vous n'avez plus qu'à dire "parle" pour qu'il pianote, "marche" pour qu'il marche, "cherche" pour qu'il se promène en disant bonjour aux avatars qu'il rencontre et "stop" pour qu'il s'arrête.

Si vous constatez que votre squelette s'enfonce dans le sol vous pouvez abaisser la position du root, ça ne détruira pas l'animation.


« Retour aux projets