Ce site est consacré principalement au langage LSL de Second Life...
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...
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.