AP7 : pilote AnimPipeline, méthodes ue4/ue5/genesis_direct, identification du squelette, mod lpr_anim_freeanims, cuisson DLC (ProjectVersion 2026.10.1.6)
Phase AP7 (CHG-221..224). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
143e29643b
commit
0016dc2197
32 changed files with 2992 additions and 146 deletions
187
AnimSources/freeanims/pack.json
Normal file
187
AnimSources/freeanims/pack.json
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"pack": "freeanims",
|
||||
"prefixe": "fap",
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"auteur": "SoerGame",
|
||||
"licence": "CC BY 4.0",
|
||||
"url": "https://www.fab.com/listings/9e93d929-36e8-45f9-b925-325db1b9385d",
|
||||
"url_auteur": "https://www.fab.com/sellers/SoerGame",
|
||||
"fichiers": [
|
||||
"Content/FreeAnimationsPack (assets Unreal)"
|
||||
],
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"exclus": [],
|
||||
"chaine": {
|
||||
"methode": "auto",
|
||||
"profil_attendu": "ue5_mannequin",
|
||||
"source": {
|
||||
"type": "uasset",
|
||||
"clips": "/Game/FreeAnimationsPack/Animations",
|
||||
"maillage": "/Game/FreeAnimationsPack/Demo/Characters/Mannequins/Meshes/SKM_Manny.SKM_Manny",
|
||||
"clip_reference": "AS_Rest"
|
||||
},
|
||||
"sortie": {
|
||||
"clips": "/Game/AnimPacks/FreeAnimationsPack",
|
||||
"rigs": "/Game/Retarget_ue5"
|
||||
},
|
||||
"corps": [
|
||||
{
|
||||
"nom": "Bridget",
|
||||
"maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp",
|
||||
"prefixe": "B_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/Idle_F_Daz"
|
||||
},
|
||||
{
|
||||
"nom": "Michael",
|
||||
"maillage": "/Game/Heavy/Daz/Michael/michael.michael",
|
||||
"prefixe": "M_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/idle_Daz"
|
||||
}
|
||||
],
|
||||
"mod": "lpr_anim_freeanims",
|
||||
"travail": "Saved/AnimPipeline/freeanims",
|
||||
"sandbox": "AnimPipeline/freeanims/sandbox"
|
||||
},
|
||||
"animations": {
|
||||
"fap_as_airkiss": {
|
||||
"clip": "AS_AirKiss",
|
||||
"nom": {
|
||||
"fr": "Baiser envoyé",
|
||||
"en": "Blow a kiss"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Geste ponctuel (probablement joué une fois) d'envoi de baiser, joué sur place ; utile pour une scène de flirt ou d'adieu.",
|
||||
"en": "One-shot gesture (probably played once) of blowing a kiss, played in place; useful for a flirting or farewell scene."
|
||||
}
|
||||
},
|
||||
"fap_as_attackinlevitation2": {
|
||||
"clip": "AS_AttackInLevitation2",
|
||||
"nom": {
|
||||
"fr": "Attaque en lévitation",
|
||||
"en": "Levitation attack"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Attaque lancée en lévitation, probablement jouée une fois, sur place ; pour un sort ou une attaque surnaturelle.",
|
||||
"en": "Attack performed while levitating, probably played once, in place; for a spell or supernatural attack."
|
||||
}
|
||||
},
|
||||
"fap_as_chilldriving": {
|
||||
"clip": "AS_ChillDriving",
|
||||
"nom": {
|
||||
"fr": "Conduite détendue",
|
||||
"en": "Relaxed driving"
|
||||
},
|
||||
"categorie": "conduite",
|
||||
"accessoire": "volant",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Pose de conduite détendue, probablement en boucle, jouée sur place ; à utiliser avec un véhicule ou un volant.",
|
||||
"en": "Relaxed driving pose, probably looping, played in place; to use with a vehicle or steering wheel."
|
||||
}
|
||||
},
|
||||
"fap_as_combohands": {
|
||||
"clip": "AS_ComboHands",
|
||||
"nom": {
|
||||
"fr": "Enchaînement de poings",
|
||||
"en": "Punch combo"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Enchaînement de coups de poing, probablement joué une fois, sur place ; pour une bagarre ou un entraînement.",
|
||||
"en": "Sequence of punches, probably played once, in place; for a fight or training scene."
|
||||
}
|
||||
},
|
||||
"fap_as_eatingonparty": {
|
||||
"clip": "AS_EatingOnParty",
|
||||
"nom": {
|
||||
"fr": "Manger en soirée",
|
||||
"en": "Eating at party"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Personnage mangeant debout lors d'une soirée, probablement en boucle, joué sur place ; pour un buffet ou une fête.",
|
||||
"en": "Character eating while standing at a party, probably looping, played in place; for a buffet or party scene."
|
||||
}
|
||||
},
|
||||
"fap_as_flyingup": {
|
||||
"clip": "AS_FlyingUp",
|
||||
"nom": {
|
||||
"fr": "Envol",
|
||||
"en": "Take off"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Décollage vers le haut, probablement joué une fois, sur place ; pour un envol ou une lévitation.",
|
||||
"en": "Upward take-off, probably played once, in place; for flying off or levitating."
|
||||
}
|
||||
},
|
||||
"fap_as_goforward": {
|
||||
"clip": "AS_GoForward",
|
||||
"nom": {
|
||||
"fr": "Marche avant",
|
||||
"en": "Walk forward"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Cycle de marche vers l'avant, probablement en boucle, joué sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Forward walk cycle, probably looping, played in place; combine with a character movement."
|
||||
}
|
||||
},
|
||||
"fap_as_rest": {
|
||||
"clip": "AS_Rest",
|
||||
"nom": {
|
||||
"fr": "Repos",
|
||||
"en": "Rest"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Posture de repos debout, probablement en boucle, jouée sur place ; pose d'attente par défaut.",
|
||||
"en": "Standing rest posture, probably looping, played in place; default waiting pose."
|
||||
}
|
||||
},
|
||||
"fap_as_rightlegkick2": {
|
||||
"clip": "AS_RightLegKick2",
|
||||
"nom": {
|
||||
"fr": "Coup de pied droit",
|
||||
"en": "Right leg kick"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Coup de pied de la jambe droite, probablement joué une fois, sur place ; pour un combat ou un entraînement.",
|
||||
"en": "Right leg kick, probably played once, in place; for a fight or training scene."
|
||||
}
|
||||
},
|
||||
"fap_as_talk": {
|
||||
"clip": "AS_Talk",
|
||||
"nom": {
|
||||
"fr": "Parler",
|
||||
"en": "Talk"
|
||||
},
|
||||
"categorie": "conversation",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Personnage qui parle debout, probablement en boucle, joué sur place ; pour un dialogue ou une conversation.",
|
||||
"en": "Character talking while standing, probably looping, played in place; for a dialogue or conversation."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,39 @@
|
|||
"exclus": [
|
||||
"A_TPose"
|
||||
],
|
||||
"chaine": {
|
||||
"methode": "auto",
|
||||
"profil_attendu": "ue4_mannequin",
|
||||
"source": {
|
||||
"type": "glb",
|
||||
"fichier": "D:/Game/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb",
|
||||
"import": "Tools/ImportQuaternius.py",
|
||||
"clips": "/Game/Quaternius/UAL",
|
||||
"maillage": "/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard",
|
||||
"clip_reference": "A_TPose"
|
||||
},
|
||||
"sortie": {
|
||||
"clips": "/Game/Quaternius/UAL",
|
||||
"rigs": "/Game/Quaternius/Retarget"
|
||||
},
|
||||
"corps": [
|
||||
{
|
||||
"nom": "Bridget",
|
||||
"maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp",
|
||||
"prefixe": "B_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/Idle_F_Daz"
|
||||
},
|
||||
{
|
||||
"nom": "Michael",
|
||||
"maillage": "/Game/Heavy/Daz/Michael/michael.michael",
|
||||
"prefixe": "M_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/idle_Daz"
|
||||
}
|
||||
],
|
||||
"mod": "lpr_anim_quaternius",
|
||||
"travail": "Saved/AnimPipeline/quaternius",
|
||||
"sandbox": "Measurement"
|
||||
},
|
||||
"animations": {
|
||||
"qua_crouch_fwd_loop": {
|
||||
"clip": "Crouch_Fwd_Loop",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[/Script/EngineSettings.GeneralProjectSettings]
|
||||
ProjectID=543F4F9E4DE525696C82F5A52303C15C
|
||||
ProjectName=LifePlay
|
||||
ProjectVersion=2026.10.1.5
|
||||
ProjectVersion=2026.10.1.6
|
||||
Description=Portage UE 5.5 - assets integres depuis LifePlay 5.9 Full Source
|
||||
|
||||
[/Script/MoviePlayer.MoviePlayerSettings]
|
||||
|
|
@ -47,6 +47,11 @@ bSkipMovies=False
|
|||
; Tout /Game doit donc etre cuit, quelle que soit la methode d'empaquetage (ERR-026).
|
||||
+DirectoriesToAlwaysCook=(Path="/Game")
|
||||
+DirectoriesToNeverCook=(Path="/Game/Heavy/New/fBlah")
|
||||
; Lot AP7 : dossiers de travail de la chaine d'animations (sources des packs, clips convertis, rigs) : livres par mods, jamais dans le jeu
|
||||
+DirectoriesToNeverCook=(Path="/Game/Quaternius")
|
||||
+DirectoriesToNeverCook=(Path="/Game/FreeAnimationsPack")
|
||||
+DirectoriesToNeverCook=(Path="/Game/AnimPacks")
|
||||
+DirectoriesToNeverCook=(Path="/Game/Retarget_ue5")
|
||||
; Lot O1 : contenu des plugins limites a l'editeur (LifePlay.uproject, TargetAllowList Editor) : jamais cuit.
|
||||
+DirectoriesToNeverCook=(Path="/Interchange")
|
||||
+DirectoriesToNeverCook=(Path="/InterchangeAssets")
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ pose ») et le mécanisme des mods d'assets (16_H3_Mods_Paquets.md).
|
|||
| Question | Situation aujourd'hui | Source |
|
||||
|---|---|---|
|
||||
| **Comment les animations entrent** | Un seul pack, importé à la main : `Tools/ImportQuaternius.py` lit un `.glb` à un chemin fixe (`D:/Game/Universal Animation Library[Standard]/…`) et écrit dans `/Game/Quaternius/UAL`. Un second pack (`Content/FreeAnimationsPack`, mannequin UE5) est présent mais n'est pas traité. | `ImportQuaternius.py` |
|
||||
| **Comment le modèle est identifié** | Il ne l'est pas : la correspondance des os est écrite en dur deux fois, pour le seul couple « mannequin UE4 (Quaternius) → Genesis 8 » : en C++ (`Links`, `TSegs`, `TransferMaps`, `DazToT` dans `LPPoseMeasure.cpp`) et en Python (`SOURCE_CHAINS`, `DAZ_CHAINS` dans `RetargetQuaternius.py`). | |
|
||||
| **Comment le modèle est identifié** | **Levé (AP7)** : `-run=LPAnimIdentify` (et `LPAnimMeasure::Identify`) compare les os du squelette aux profils `Tools/anim/profiles/*.json` (score par profil, os manquants, méthode associée) ; à égalité, le profil le plus complet l'emporte (le mannequin UE5 contient tous les noms UE4). Les tables ne sont plus en dur. | CHG-221 |
|
||||
| **Le pack contient-il un modèle de référence** | Quaternius : oui, `UAL1_Standard` (maillage, squelette, repos en pose T). FreeAnimationsPack : oui, `SKM_Manny` / `SKM_Quinn`, plus un clip `MM_T_Pose` et son propre `RTG_Mannequin`. | `Content/Quaternius/UAL`, `Content/FreeAnimationsPack/Demo` |
|
||||
| **Calques** | Le calque de pose (CHG-203) est le squelette cible posé par la formule, à partir du squelette source du pack : il utilise déjà le modèle de référence du pack (son squelette au repos), pas un modèle à nous. | mode Mesure du plugin `LPSandbox` (`LPSandboxMeasure*.cpp`, ex-`LPPairSandbox.cpp`) |
|
||||
| **Conversion** | Retargeter Unreal généré par `RetargetQuaternius.py`, pose de retarget lue dans `Saved/Measurement/retarget_pose_<Corps>.txt` (CHG-204). Écriture : entrée du mode Mesure (`Tools/debug/LifePlay_measure_sandbox.bat`, `Saved/sandbox_mode.txt` = `Mesure`) ou commandlet `-run=LPAnimEval` (`-PoseDir` = `Saved/Measurement` par défaut). Module de mesure : `LPAnimMeasure` dans l'addon `LifePlay-Sandbox` (plugin `LPSandbox`, lot S3). Les os *Twist* sont répartis après cuisson (`-run=LPAnimTwist`, AP3). | `RetargetQuaternius.py`, `21_Sandbox_Addon.md` |
|
||||
| **Évaluation** | **Interactive :** tournée sandbox Mesure (`Saved/Measurement/tour.csv`, captures `tour/`, vignettes, planches ; CHG-202, AP6). **Headless :** `-run=LPAnimEval` (`LifePlayEditorTools`, dépend de `LPAnimMeasure`) — rapports sous `Saved/AnimPipeline/<run>/` ; contrôles indépendants AP4 (`controles_*.csv`, `rapport.json`). | CHG-206 à CHG-208, `S6-VAL.md` |
|
||||
| **Conversion** | Retargeter généré par `Tools/anim/RetargetPack.py` (ex-`RetargetQuaternius.py`), piloté par `Saved/anim_retarget.json` ; la méthode (`ue4` pose T, `ue5` pose A, `genesis_direct`) est choisie par `-Methode=<id ou auto>` ; pose de retarget écrite par `-run=LPAnimEval` (`-PoseDir`, `Saved/Measurement` par défaut) ou la sandbox Mesure ; os *Twist* répartis après cuisson (`-run=LPAnimTwist`). Module de mesure : `LPAnimMeasure` (addon `LifePlay-Sandbox`, `21_Sandbox_Addon.md`). | CHG-204, CHG-221, CHG-222 |
|
||||
| **Évaluation** | **Interactive :** tournée sandbox Mesure (`tour.csv`, captures, vignettes, planches ; CHG-202, AP6). **Headless :** `-run=LPAnimEval` — rapports sous `Saved/AnimPipeline/<run>/`, contrôles AP4 (`controles_*.csv`, `rapport.json`). **Enchaînée (AP7) :** `Tools/anim/AnimPipeline.ps1` (`BuildMod.ps1 -Anim <pack>`) exécute identification, calque, conversion, évaluation, livraison et catalogue ; passé sur Quaternius et FreeAnimationsPack. | CHG-206 à CHG-208, CHG-223 |
|
||||
| **Livraison** | Les clips convertis sont dans le **contenu du jeu** (`/Game/Quaternius/UAL/B_*`, `M_*`), adressés par un chemin en dur (`LPAnimRedirect::ClipPath`). Ils ne passent pas par un mod. `BuildMod.ps1 -Retarget` ne vérifie que la présence d'une ligne de journal. | `LPAnimRedirect.cpp`, `BuildMod.ps1` |
|
||||
| **Appel depuis un scénario** | Un scénario désigne une animation par une clé courte : `ANIMATION: write` (`.lpaction`), `Actor.animate(martialart)` (`.lpscene`). Le jeu lui associe les assets `<clé>_F` (corps féminin) et `<clé>_M` (corps masculin), par exemple `Content/Heavy/New3/Anims/Daily/martialart_F` / `_M` ; la même convention sert aux paires de couple (`LPShowCoupleSubsystem`). Les clips convertis, nommés `B_<clip>` / `M_<clip>`, ne sont donc **pas appelables** par un scénario. Le jeu résout la clé par `Person::PlayDaily`, qui cherche dans `Room.Dailies` (tableau fixe de clips, sans registre d'assets) ; les clips des mods y sont ajoutés par `ULPModDailiesSubsystem` (CHG-209). | `vin_Base/Actions`, `vin_Base/Scenes` |
|
||||
| **Information pour le moddeur** | Aucune : pour connaître la durée, la posture ou l'usage d'un clip, il faut l'ouvrir dans l'éditeur. | |
|
||||
|
||||
### Écarts à combler
|
||||
|
||||
1. Pas d'identification du squelette : un pack Mixamo, UE5, CC4 ou Genesis 9 serait converti avec une table fausse.
|
||||
2. Correspondance dupliquée (C++ et Python) : une modification oubliée d'un côté fausse le résultat sans erreur.
|
||||
3. ~~La pose de retarget dépend d'une ouverture manuelle de la sandbox.~~ **Levé (AP2 / S6)** : `-run=LPAnimEval` écrit les mêmes fichiers sans fenêtre ; la sandbox reste utile pour le contrôle visuel.
|
||||
1. ~~Pas d'identification du squelette : un pack Mixamo, UE5, CC4 ou Genesis 9 serait converti avec une table fausse.~~ **Levé (AP7)** : `LPAnimIdentify` et profils (CHG-221).
|
||||
2. ~~Correspondance dupliquée (C++ et Python) : une modification oubliée d'un côté fausse le résultat sans erreur.~~ **Levé (AP1, CHG-205)** : profils JSON lus par le C++ et le Python.
|
||||
3. ~~La pose de retarget dépend d'une ouverture manuelle de la sandbox.~~ **Levé (AP2 / S6, confirmé AP7)** : `-run=LPAnimEval` écrit les mêmes fichiers sans fenêtre ; la sandbox reste utile pour le contrôle visuel.
|
||||
4. Torsion des membres non répartie entre *Bend* et *Twist* (peau de l'avant-bras).
|
||||
5. Michael converti mais jamais mesuré.
|
||||
6. Livraison hors mod : impossible de distribuer un pack sans recuire le jeu.
|
||||
|
|
@ -56,6 +56,8 @@ déclarer sa conversion A → T dans son profil (comme les réglages Daz). Un co
|
|||
|
||||
Seule la **table de correspondance** change d'un squelette à l'autre : elle devient une donnée (profil), plus du code.
|
||||
|
||||
**Méthodes de transformation (AP7).** Le convertisseur choisit un profil de transformation (`Tools/anim/methodes/*.json`, `-Methode=<id ou auto>`) en quatre parties : 1) la pose du mannequin source, A ou T, est le calque d'origine ; 2) Bridget et Michael sont placés dans la même pose, le calque étant ajusté à leur morphologie ; 3) la méthode est choisie d'après le profil identifié ; 4) la transformation est appliquée. Méthodes : `ue4` (pose T, mannequin UE4 / Quaternius), `ue5` (pose A : calques = repos du mannequin UE5 et de Genesis, ajusté à la morphologie), `genesis_direct` (copie Daz, déclarée factice). Règle d'égalité : à score égal entre profils, le plus complet l'emporte (le mannequin UE5 contient tous les noms UE4 et était pris pour UE4 à 100 %). Pour UE5 la conversion se fait A → A : aucun angle A → T n'est inventé, les deux calques sont dans la même pose de repos. Borne des réglages : 30° pour le corps, 45° pour les doigts (`-SeuilReglageDoigts`), la main du mannequin UE5 étant au repos légèrement fermée (ajustement des doigts ≈ 31°). Voir CHG-221.
|
||||
|
||||
---
|
||||
|
||||
## 3. Chaîne cible
|
||||
|
|
@ -76,6 +78,8 @@ Rien n'est supprimé : un résultat précédent est déplacé dans `precedent_<d
|
|||
| 6 | **Catalogue** | Une fiche par animation (§ 5), calculée sur les clips produits et complétée par `pack.json`. | Une fiche par clé et par clip ; clés uniques dans le mod **et** sans collision avec les clés du jeu ni des mods connus ; champs obligatoires remplis ; vignettes présentes. |
|
||||
| 7 | **Livraison** | Mod d'assets (H3) : clips renommés à la convention du jeu, `<clé>_F` / `<clé>_M`, sous `/<Mod>/Anims/…` ; module texte `.lpmod` ; catalogue joint ; cuisson DLC, contrôle du conteneur et archive `.zip` (étapes existantes de `BuildMod`). La clé peut contenir `_` (coupure au dernier `_`, CHG-209) ; outil : `Tools\anim\DeliverAnims.ps1`. | Conteneur sans asset du jeu ; archive testée ; **essai de chargement** : jeu de test lancé avec le mod, chaque clé du catalogue appelée par `animate()` sur un personnage féminin et un masculin (journal). |
|
||||
|
||||
Réalisé par `Tools/anim/AnimPipeline.ps1` (CHG-223) ; ordre réel des étapes 6 et 7 : la livraison dans le mod précède les fiches et le catalogue, puis vient la cuisson DLC.
|
||||
|
||||
Le jeu résout les clés par `Room.Dailies` ; les clips des mods y sont ajoutés par `ULPModDailiesSubsystem` (CHG-209).
|
||||
`LPAnimRedirect` ne sert plus qu'à la sandbox et au personnage piloté.
|
||||
|
||||
|
|
@ -154,6 +158,6 @@ Le pilote refuse de livrer un mod dont le catalogue n'a pas été produit dans l
|
|||
| AP4 ✔ | Contrôles indépendants (§ 4) et rapport HTML | seuils étalonnés sur les 43 clips — **fait, CHG-208** (`-run=LPAnimEval`, `Tools/anim/RapportControles.py`) ; 0 rejeté, 8 clips à revoir (4 par corps : `Death01`, `Fixing_Kneeling`, `Roll` au sol, `Jump_Start` au saut) |
|
||||
| AP5 ✔ | Livraison en mod : clips renommés `<clé>_F` / `<clé>_M` sous `/<Mod>/Anims`, ajout à `Room.Dailies` et correctif de `Person::PlayDaily`, essai de chargement par `animate()` | jeu de test avec le mod Quaternius, clés appelées depuis un scénario d'essai — **fait, CHG-209** ; essai empaqueté reporté (nouvelle version de référence nécessaire) |
|
||||
| AP6 ✔ | Catalogue (§ 5) : fiches calculées, `pack.json`, vignettes, `catalogue.json` et `catalogue.html` | une fiche par clip, champs calculés relus sur 5 clips témoins (marche, assis, pistolet, danse, chute) — **fait, CHG-210** ; taille cuite en AP7 |
|
||||
| AP7 | Pilote `AnimPipeline.ps1` + `BuildMod -Anim`, identification, deuxième pack (FreeAnimationsPack, mannequin UE5) | pack UE5 converti, évalué, catalogué et livré de bout en bout sans intervention |
|
||||
| AP7 ✔ | Pilote `AnimPipeline.ps1` + `BuildMod -Anim`, identification, méthodes `ue4` / `ue5`, deuxième pack (FreeAnimationsPack, mannequin UE5) | pack UE5 converti, évalué, catalogué et livré de bout en bout sans intervention — **fait, CHG-221 à 224** ; cuisson DLC contre la version d'essai `2026.10.1.6` et essai en jeu empaqueté validés (CHG-224) |
|
||||
|
||||
Chaque lot suit `10_Protocole_Operations.md` : instantané, journal `CHG`, retour arrière par `Restore.ps1`.
|
||||
|
|
|
|||
|
|
@ -117,12 +117,12 @@ D:\Game\
|
|||
|
||||
- **Liaison** : `LifePlay.uproject` déclare `"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` (chemin relatif au `.uproject`) et active `LPSandbox` dans `Plugins`. UBT et l'éditeur y trouvent `LPSandbox.uplugin` (modules `LPSandbox` et `LPAnimMeasure`) ; les DLL du plugin sont compilées avec le jeu (`Build.bat LifePlayEditor Win64 Development`).
|
||||
- **Pourquoi pas un submodule** : l'addon est un `uproject` mince qui contient le plugin sous `Plugins/LPSandbox/`. Un submodule monté sur `Plugins/LPSandbox` du jeu donnerait `Plugins/LPSandbox/Plugins/LPSandbox/` (plugin imbriqué, non scanné au bon niveau) ; un submodule `External/LifePlay-Sandbox` dupliquerait l'addon à côté du sibling et rendrait ambigu l'exemplaire chargé. Un clone sibling évite les deux problèmes.
|
||||
- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `f09c42e5a2bc613cf269cce76c878461ca41c8b7`, `main`, S7-T2 uplugin ; précédents : `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
|
||||
- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `a3b1b897715b05b22d75b79d53637e58b6698c2c`, `main`, AP7 méthodes de transformation et sandbox multi-pack ; précédents : `f09c42e5a2bc613cf269cce76c878461ca41c8b7` S7-T2 uplugin, `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
|
||||
|
||||
```powershell
|
||||
cd D:\Game
|
||||
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
|
||||
git -C LifePlay-Sandbox checkout f09c42e5a2bc613cf269cce76c878461ca41c8b7
|
||||
git -C LifePlay-Sandbox checkout a3b1b897715b05b22d75b79d53637e58b6698c2c
|
||||
# le jeu doit être cloné à côté : D:\Game\LifePlay-master 5.5
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,10 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
|
|||
| CHG-155 | 2026-10-05 | ShowCouple | Un lit est reconnu par son nom. Il ne bloque un déplacement que vers le bas, pour se poser ; le retour horizontal n'est plus arrêté et le contact ne renvoie plus la boîte | après avoir quitté le lit, la boîte ne pouvait plus y revenir | revenir au test `DoesPackageExist` et au filtre du flanc seul |
|
||||
| CHG-156 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` : bouton « Clavier » ajouté à l'exécution dans `MainMenu.SettingsScrollBox` ; panneau pour changer, ajouter ou retirer une touche (actions et axes clavier). Écrit dans `Saved/Config/Windows`, pas dans `GameSettings.sav` | pouvoir réaffecter les touches, dont Q, sans modifier les Blueprints | retirer `LPKeyboardSubsystem.h/.cpp` et la dépendance `UMG` |
|
||||
| CHG-158 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : onglets Audio, Video, Lewd, General, Keyboard, Misc au-dessus de `SettingsScrollBox`. Les lignes du Blueprint restent en place et sont montrées selon leur texte ; le reste va dans Misc. Keyboard affiche la liste des touches | la liste unique ne montrait pas le clavier | retirer `LPSettingsSubsystem.h/.cpp` |
|
||||
| CHG-224 | 2026-10-07 | AP7 | Cuisson DLC et essai en jeu empaqueté. `Config/DefaultGame.ini` : `ProjectVersion=2026.10.1.6` (version de référence d'essai, non publiée) ; `+DirectoriesToNeverCook` pour `/Game/Quaternius`, `/Game/FreeAnimationsPack`, `/Game/AnimPacks`, `/Game/Retarget_ue5` (sources de travail, jamais dans le jeu). `Tools/anim/DeliverAnims.py` : à la copie dans le mod, les réglages de compression qui pointent sur `/Engine/` sont remis à vide (les clips d'un pack UE5 référençaient `/Engine/Animation/DefaultAnim*CompressionSettings` : cuisson DLC refusée) ; la compression par défaut du projet s'applique, comme pour Quaternius | lot AP7, étape 7. Empaquetage Shipping `2026.10.1.6` (`D:\Game\LifePlay_Build_5.5_Shipping_AP7\Windows`, `Releases/2026.10.1.6`) ; mods cuits contre cette version : `lpr_anim_quaternius` (84 paquets, zip 15 798 332 o), `lpr_anim_freeanims` (20 paquets, 0 référence moteur, zip 4 416 299 o) dans `D:\Game\LifePlay_Publication\Mods\` ; module d'essai `lpr_anim_essai` copié dans le seul jeu d'essai (hors du projet : `LifePlay_Backup_5.5/AP7/hors_build`). **Essai utilisateur en jeu empaqueté : animations jouées** (`qua_*`, `fap_*`, témoin `dance`) | `.\Tools\Restore.ps1 -Lot AP7` ; supprimer `D:\Game\LifePlay_Build_5.5_Shipping_AP7` et `Releases/2026.10.1.6` ; rien n'est commité dans le dépôt du jeu |
|
||||
| CHG-223 | 2026-10-07 | AP7 | Pilote. `Tools/anim/AnimPipeline.ps1` (étapes 0 inventaire, 1 import, 2 identification, 3 calque, 4 conversion + torsion, 5 évaluation + rapport HTML + vignettes et planches par la sandbox, 6 livraison dans le mod + fiches + catalogue, 7 cuisson DLC ; rapport JSON par étape dans `Saved/AnimPipeline/<pack>/etapes`, `etat.json`, arrêt au premier contrôle en échec, reprise `-From`, ancien contenu déplacé dans `precedent_<date>`, `-DryRun`, `-SansCuisson`) ; `BuildMod.ps1 -Anim <pack>` ; `pack.json` étendu (bloc `chaine` : méthode, profil attendu, source glb|uasset, sorties, corps et gabarits Daz `Idle_F_Daz` / `idle_Daz`, mod, dossier de travail, sortie sandbox) ; `AnimSources/freeanims/pack.json` (licence CC BY 4.0, auteur SoerGame, page Fab, page de l'auteur `url_auteur`, 10 animations déclarées) ; outils de livraison et de catalogue génériques (`DeliverAnims -Pipeline -Pack -Vignettes -Planches`, textes du mod et lien d'auteur tirés de `pack.json`, adresse de la source dans `CREDITS.txt`). Défauts corrigés pendant les passages réels : console `start /wait` restée ouverte (lancement par `cmd /c`), lancement de la sandbox par un tuyau qui attendait sa fermeture | lot AP7. Quaternius de bout en bout (reprises à l'étape 4 puis 5 après corrections) : mesures, verdicts, fiches et catalogue identiques au passage précédent (42 fiches, 42 vignettes, 42 planches) ; FreeAnimationsPack de bout en bout en 10 min, code 0 : mod `lpr_anim_freeanims` (10 clips × 2, `.lpmod` CC BY 4.0, crédits), catalogue de 10 fiches. Étape 7 (cuisson DLC) non faite : la version de référence `Releases/2026.10.1.5` (V1.5) ne contient ni le correctif de `Person` ni `LPModDailies` ; nouvel empaquetage à décider avec l'utilisateur | `.\Tools\Restore.ps1 -Lot AP7` (jeu) et `.\Tools\Restore.ps1 -Lot AP7-addon -ProjectRoot D:\Game\LifePlay-Sandbox` (sources de l'addon ; les commits de l'addon restent dans son historique) ; rien n'est commité dans le dépôt du jeu |
|
||||
| CHG-222 | 2026-10-07 | AP7 | Conversion et sandbox. `Tools/RetargetQuaternius.py` déplacé en `Tools/anim/RetargetPack.py`, piloté par `Saved/anim_retarget.json` (méthode, maillage source, dossiers, corps, exclusions, dossier des poses ; défauts = Quaternius, noms d'assets inchangés ; rigs suffixés pour les autres méthodes) ; jeton de cuisson (BuildMod ne retient que les lignes `RETARGET … jeton <j>`). `BuildMod.ps1` : chemins entre guillemets au lancement de l'éditeur (le dossier contient une espace ; l'éditeur se fermait sans journal), lecture complète des journaux modifiés depuis le lancement (l'ancienne lecture à partir de la taille du précédent `LifePlay.log` sautait la ligne de résultat), attente de fermeture de l'éditeur portée à 10 min, `-RetargetConfig`, `-Anim`. Addon `LifePlay-Sandbox` (commits `a3b1b89` puis `159afbf`, pin mis à jour dans `etat/sandbox/S3/S3-INT.md`, `21_Sandbox_Addon.md` et README de l'addon) : `[Sandbox.Pack]` + surcharge `Saved/sandbox_pack.ini` (mode Mesure seulement), `LPAnimRedirect::BodyClipPath`, clip initial du pack, tournée `vignettes planches quitter` (enchaînement et fermeture). Piège rencontré : des DLL périmées du plugin dans `LifePlay-Sandbox/Plugins/LPSandbox/Binaries` (même BuildId) étaient chargées à la place des neuves ; dossier déplacé (non supprimé) dans `LifePlay_Backup_5.5/AP7-addon/Binaries_perimes_2036` | lot AP7 : FreeAnimationsPack (10 clips) converti en `ue5` : 0 rejeté, 0 à revoir, écart 0,0006 %, torsion 0,015° ; **validé visuellement par l'utilisateur** dans la sandbox | `.\Tools\Restore.ps1 -Lot AP7` (jeu) et `.\Tools\Restore.ps1 -Lot AP7-addon -ProjectRoot D:\Game\LifePlay-Sandbox` (sources de l'addon ; les commits de l'addon restent dans son historique) ; rien n'est commité dans le dépôt du jeu |
|
||||
| CHG-221 | 2026-10-07 | AP7 | Méthodes de transformation et identification du squelette. Profil `Tools/anim/profiles/ue5_mannequin.json` (SK_Mannequin UE5, colonne 5 os, cou 2, réglages A→T optionnels) ; méthodes `Tools/anim/methodes/ue4.json` (pose T, ex-« Quaternius »), `ue5.json` (pose A : calques = repos du mannequin UE5 et de Genesis, ajusté à la morphologie), `genesis_direct.json` (copie Daz, déclarée factice). Principe (demande de l'utilisateur) : 1) la pose du mannequin, A ou T, est le calque d'origine ; 2) Bridget et Michael sont placés dans la même pose, calque ajusté à leur morphologie ; 3) le convertisseur choisit le profil de transformation ; 4) application. Addon `LPAnimMeasure` : `SetMethod`, `MethodForProfile`, calque par méthode (`MakeTPose`), cache d'ajustement par méthode, identification à égalité → profil le plus complet (le mannequin UE5 contient tous les noms UE4 : il était pris pour UE4 à 100 %). Commandlets du jeu : `LPAnimIdentify` (nouveau : scores par profil, os manquants, squelette, méthode ; contrôle croisé avec le module de mesure), `-Methode=<id|auto>` sur `LPAnimEval` et `LPAnimTwist` (`Source/LifePlayEditorTools/LPAnimMethodSelect.h`), `-BodyClipDir`, `-Templates`, `-PoseOnly`, borne des réglages séparée pour les doigts `-SeuilReglageDoigts=45` (corps : 30 ; la main du mannequin UE5 est au repos légèrement fermée : ajustement des doigts ≈ 31°) | lot AP7 de `20_Chaine_Animations.md`. Non-régression : Quaternius en `auto` → `ue4`, toutes valeurs et verdicts identiques à AP4 (écart de torsion 4e-5°) | `.\Tools\Restore.ps1 -Lot AP7` (jeu) et `.\Tools\Restore.ps1 -Lot AP7-addon -ProjectRoot D:\Game\LifePlay-Sandbox` (sources de l'addon ; les commits de l'addon restent dans son historique) ; rien n'est commité dans le dépôt du jeu |
|
||||
| CHG-220 | 2026-10-06 | S8 | `Docs/Migration/22_Eval_Agents_Sandbox.md` (nouveau) ; `Docs/Migration/etat/sandbox/S8/S8-VAL.md` (nouveau) ; `Docs/Migration/21_Sandbox_Addon.md` (§ Acceptation : S8 coché, push addon coché, deux cases ouvertes ajoutées `BuildMod -Target` et commit côté jeu ; ligne S8 du tableau des lots) ; `Saved/Logs/S8-VAL-runtime-game.log` (copie locale, non versionnée) | lot S8-T1/T2 : bilan du mode multi-agents + multi-modèles sur S0–S7 (lots tenus / coupés, consignes et reprises, adéquation modèle ↔ fiche : 11 fiches sur 27 tracent le modèle réel, S3-T3 sous-classé, S2-T2 sur-classé ; erreurs de rôle ; friction git : submodule abandonné, jeu jamais commité, trois identités git) ; décision **GO conditionnel** et 3 recommandations (politique modèles tracée, clôture git par lot, séparation VAL / DOC + registre des reports) ; validation S8-VAL **PASS** : `Tools/debug/LifePlay_pair_sandbox.bat` lancé tel quel, plugin monté, `LPPairSandboxGameMode`, 16 lignes `LogLPSandbox`, boîte `AgMakeout` identique à S5 / S7-VAL, 0 crash ; constat : `origin/main` addon = `dae6206` (pin `f09c42e` poussé) | aucun changement de code C++ ni de configuration | retirer `22_Eval_Agents_Sandbox.md`, `etat/sandbox/S8/`, cette entrée ; rétablir § Acceptation et ligne S8 de doc 21 (état S7) |
|
||||
| CHG-219 | 2026-10-06 | S7 | `LifePlay.uproject` (plugin `LPSandbox` : `Optional: true`) ; repo addon `LifePlay-Sandbox` `Plugins/LPSandbox/LPSandbox.uplugin` (`SupportedTargetPlatforms: ["Win64"]`, module `LPSandbox` : `TargetConfigurationDenyList: ["Shipping"]`, description ; commit local `f09c42e`, non poussé) ; `Docs/Migration/21_Sandbox_Addon.md` (§ Acceptation, ligne S7, pin), `Docs/Migration/etat/sandbox/S3/S3-INT.md` (pin) ; `Docs/Migration/etat/sandbox/S7/S7-SPEC.md`, `S7-IMPL-L.md`, `S7-CART.md`, `S7-VAL.md` (nouveaux) ; `Saved/Logs/S7-VAL-*.log` (copies locales, non versionnées) | lot S7-T1..T4 : contrat d'isolation du plugin (aucun include / `LIFEPLAY_API` / module `LifePlay` : 0 résultat ; `Person` en lecture de CDO seule, aucun `GI` / `Room` ; chemins par config) ; `LPSandbox` exclu du Shipping (`TargetAllowList: ["Editor"]` rejeté : le sandbox tourne en `-game`), `LPAnimMeasure` conservé car requis par `LifePlay.Build.cs` ; validation S7-VAL **PASS** : build `LifePlayEditor Development` à jour, plan UBT Game Development (6 occurrences `LPSandbox`) / Shipping (0), smoke Pair ~1 min 45 s (boîte AgMakeout identique à S5-VAL, 0 crash) ; limites : pas de compilation Shipping complète, commit addon non poussé, `Optional` n'évite pas l'échec du build sans clone sibling | aucun changement de code C++ | `git revert f09c42e` dans l'addon, retirer `"Optional": true` du `.uproject`, remettre le pin `c1fdd29`, retirer `S7/`, § Acceptation et l'entrée journal |
|
||||
| CHG-218 | 2026-10-06 | S6 | `Docs/Migration/20_Chaine_Animations.md`, `Docs/Migration/09_Commandes_Utiles.md`, `Docs/Migration/21_Sandbox_Addon.md` (lot S6), `Docs/Migration/etat/sandbox/S6/S6-VAL.md` (nouveau) ; `Saved/AnimPipeline/S6-VAL/`, `Saved/Logs/S6-VAL-LPAnimEval.log` (copies locales, non versionnées) | lot S6-T1…T3 : confirmation que `LifePlayEditorTools.Build.cs` dépend déjà de `LPAnimMeasure` (module addon, aucun include cassé, 0 fichier `Source/LPAnimMeasure/`) ; build `LifePlayEditor Win64 Development` à jour ; doc chaîne animations et commandes alignées (plugin `LPSandbox`, bats `Tools/debug`, sorties `Saved/Measurement`, commandlets `LPAnimEval` / `LPAnimTwist` / `LPAnimCatalog`) ; validation S6-VAL **PASS** : `-run=LPAnimEval -Clips=Idle_Loop -Instants=1` (~1,3 s, code 0, `rapport.json` valide, `LogLPAnimMeasure` actif) ; bat Mesure non rejoué (déjà PASS en S2-VAL) | aucun changement de code C++ | retirer `S6-VAL.md`, entrée journal, rétablir les trois docs |
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"AdditionalPluginDirectories": [
|
||||
"../LifePlay-Sandbox/Plugins"
|
||||
],
|
||||
"Plugins": [
|
||||
{
|
||||
"Name": "LPSandbox",
|
||||
"Enabled": true,
|
||||
"Optional": true
|
||||
"Optional": true,
|
||||
"SupportedTargetPlatforms": [
|
||||
"Win64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ModdingExtra",
|
||||
|
|
@ -201,6 +201,9 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"AdditionalPluginDirectories": [
|
||||
"../LifePlay-Sandbox/Plugins"
|
||||
],
|
||||
"TargetPlatforms": [
|
||||
"Windows"
|
||||
]
|
||||
|
|
|
|||
106
ModSources/lpr_anim_freeanims/Module/Animations/catalogue.html
Normal file
106
ModSources/lpr_anim_freeanims/Module/Animations/catalogue.html
Normal file
File diff suppressed because one or more lines are too long
761
ModSources/lpr_anim_freeanims/Module/Animations/catalogue.json
Normal file
761
ModSources/lpr_anim_freeanims/Module/Animations/catalogue.json
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
{
|
||||
"pack": "freeanims",
|
||||
"mod": "lpr_anim_freeanims",
|
||||
"version": "1.0",
|
||||
"genere": "2026-10-07T02:38:49",
|
||||
"nombre_animations": 10,
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"animations": [
|
||||
{
|
||||
"cle": "fap_as_airkiss",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_airkiss)",
|
||||
"action": "ANIMATION: fap_as_airkiss"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Baiser envoyé",
|
||||
"en": "Blow a kiss"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_airkiss_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_airkiss_M"
|
||||
},
|
||||
"duree_s": 10.3,
|
||||
"images": 310,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_airkiss.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 2.3
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1262,
|
||||
"masculin": 1253,
|
||||
"total": 2515
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 36.57
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 8,
|
||||
"cm_s": 1,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "interaction",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Geste ponctuel (probablement joué une fois) d'envoi de baiser, joué sur place ; utile pour une scène de flirt ou d'adieu.",
|
||||
"en": "One-shot gesture (probably played once) of blowing a kiss, played in place; useful for a flirting or farewell scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "FFBE444E7D7717C9DFD6FD4E3BD7DD706F8BB600E8AB05239CBEF9078652693A"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_attackinlevitation2",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_attackinlevitation2)",
|
||||
"action": "ANIMATION: fap_as_attackinlevitation2"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Attaque en lévitation",
|
||||
"en": "Levitation attack"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_attackinlevitation2_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_attackinlevitation2_M"
|
||||
},
|
||||
"duree_s": 7.07,
|
||||
"images": 213,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_attackinlevitation2.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 3.4
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 944,
|
||||
"masculin": 934,
|
||||
"total": 1878
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 6.74
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 6,
|
||||
"cm_s": 1,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 0,
|
||||
"droite": 0
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "magie",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Attaque lancée en lévitation, probablement jouée une fois, sur place ; pour un sort ou une attaque surnaturelle.",
|
||||
"en": "Attack performed while levitating, probably played once, in place; for a spell or supernatural attack."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.01
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "D909B0E97115A819BCCA61CC8B1D4EE819DB91CF6D471EFCF8E87C6987E8D0F1"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_chilldriving",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_chilldriving)",
|
||||
"action": "ANIMATION: fap_as_chilldriving"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Conduite détendue",
|
||||
"en": "Relaxed driving"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_chilldriving_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_chilldriving_M"
|
||||
},
|
||||
"duree_s": 6.7,
|
||||
"images": 202,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_chilldriving.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 3.6
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 908,
|
||||
"masculin": 898,
|
||||
"total": 1806
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 3.88
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 1,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "assis",
|
||||
"posture_origine": "calculee",
|
||||
"hauteur_assise_cm": 40
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "volant",
|
||||
"categorie": "conduite",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Pose de conduite détendue, probablement en boucle, jouée sur place ; à utiliser avec un véhicule ou un volant.",
|
||||
"en": "Relaxed driving pose, probably looping, played in place; to use with a vehicle or steering wheel."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "5A74F7DA765AB938991A14FFF3E77DB6A18C12D025D810D598DBDD936EB67E24"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_combohands",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_combohands)",
|
||||
"action": "ANIMATION: fap_as_combohands"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Enchaînement de poings",
|
||||
"en": "Punch combo"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_combohands_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_combohands_M"
|
||||
},
|
||||
"duree_s": 2.2,
|
||||
"images": 67,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_combohands.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 10.9
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 517,
|
||||
"masculin": 507,
|
||||
"total": 1025
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 56.24
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 1,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 93
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "combat",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Enchaînement de coups de poing, probablement joué une fois, sur place ; pour une bagarre ou un entraînement.",
|
||||
"en": "Sequence of punches, probably played once, in place; for a fight or training scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "012073AFF1B80B99F9C15CBAA1BA428CCCB1B16C3C21746E3182956A4BE967A4"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_eatingonparty",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_eatingonparty)",
|
||||
"action": "ANIMATION: fap_as_eatingonparty"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Manger en soirée",
|
||||
"en": "Eating at party"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_eatingonparty_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_eatingonparty_M"
|
||||
},
|
||||
"duree_s": 23.7,
|
||||
"images": 712,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_eatingonparty.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 1.0
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 2581,
|
||||
"masculin": 2571,
|
||||
"total": 5152
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 19.28
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 10,
|
||||
"cm_s": 0,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "interaction",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Personnage mangeant debout lors d'une soirée, probablement en boucle, joué sur place ; pour un buffet ou une fête.",
|
||||
"en": "Character eating while standing at a party, probably looping, played in place; for a buffet or party scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "8D06644A66882A9CD6A5D9CF112353409FADAF453A397DBBAC99E177C0753FBA"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_flyingup",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_flyingup)",
|
||||
"action": "ANIMATION: fap_as_flyingup"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Envol",
|
||||
"en": "Take off"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_flyingup_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_flyingup_M"
|
||||
},
|
||||
"duree_s": 2.7,
|
||||
"images": 82,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_flyingup.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 8.9
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 514,
|
||||
"masculin": 504,
|
||||
"total": 1018
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 68.13
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 1,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 43,
|
||||
"droite": 43
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "levee",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "magie",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Décollage vers le haut, probablement joué une fois, sur place ; pour un envol ou une lévitation.",
|
||||
"en": "Upward take-off, probably played once, in place; for flying off or levitating."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "327291EFF0B154B719CE85A07AD11B4DED45F4DB16787A5DE1BA3960092BA8C6"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_goforward",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_goforward)",
|
||||
"action": "ANIMATION: fap_as_goforward"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Marche avant",
|
||||
"en": "Walk forward"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_goforward_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_goforward_M"
|
||||
},
|
||||
"duree_s": 3.37,
|
||||
"images": 102,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_goforward.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 7.1
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 579,
|
||||
"masculin": 569,
|
||||
"total": 1149
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 4.29
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 0,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "locomotion",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Cycle de marche vers l'avant, probablement en boucle, joué sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Forward walk cycle, probably looping, played in place; combine with a character movement."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "44363245CBE7C209178B3292D49B97B43F2DF0DBCC4FBC5B58EA230B8C48E469"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_rest",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_rest)",
|
||||
"action": "ANIMATION: fap_as_rest"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Repos",
|
||||
"en": "Rest"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_rest_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_rest_M"
|
||||
},
|
||||
"duree_s": 11.47,
|
||||
"images": 345,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_rest.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 2.1
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1377,
|
||||
"masculin": 1357,
|
||||
"total": 2734
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 4.26
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 1,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "allonge",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "repos"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "attente",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Posture de repos debout, probablement en boucle, jouée sur place ; pose d'attente par défaut.",
|
||||
"en": "Standing rest posture, probably looping, played in place; default waiting pose."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "513BEEE6DC9431E52D66DC07D26C88621C6A6DFF465DEF42F81E711E2A5AE3DF"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_rightlegkick2",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_rightlegkick2)",
|
||||
"action": "ANIMATION: fap_as_rightlegkick2"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Coup de pied droit",
|
||||
"en": "Right leg kick"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_rightlegkick2_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_rightlegkick2_M"
|
||||
},
|
||||
"duree_s": 1.1,
|
||||
"images": 34,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_rightlegkick2.jpg",
|
||||
"images": 13,
|
||||
"taille": 256,
|
||||
"ips": 11.8
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 387,
|
||||
"masculin": 377,
|
||||
"total": 764
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 55.01
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 11,
|
||||
"cm_s": 10,
|
||||
"sens": "avant"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 0,
|
||||
"droite": 38
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "combat",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Coup de pied de la jambe droite, probablement joué une fois, sur place ; pour un combat ou un entraînement.",
|
||||
"en": "Right leg kick, probably played once, in place; for a fight or training scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "FA818881DD479BA94192C8C1BA8A8220E0E2D94432D1E04E12E59F998F0129F0"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_talk",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_talk)",
|
||||
"action": "ANIMATION: fap_as_talk"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Parler",
|
||||
"en": "Talk"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_talk_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_talk_M"
|
||||
},
|
||||
"duree_s": 14.07,
|
||||
"images": 423,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_talk.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 1.7
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1633,
|
||||
"masculin": 1623,
|
||||
"total": 3256
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 28.35
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "conversation",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Personnage qui parle debout, probablement en boucle, joué sur place ; pour un dialogue ou une conversation.",
|
||||
"en": "Character talking while standing, probably looping, played in place; for a dialogue or conversation."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.01
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "5C9CB1D57C27D542A2DC131F4C53EDA4171BD96F8FD5B9CD831566AA86563A75"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
ModSources/lpr_anim_freeanims/Module/CREDITS.txt
Normal file
2
ModSources/lpr_anim_freeanims/Module/CREDITS.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Credits and licenses of the assets in Free Animations Pack (SoerGame, Fab ; mannequin UE5) (one line per asset: title, author, license, source URL, changes).
|
||||
- Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay, https://www.fab.com/listings/9e93d929-36e8-45f9-b925-325db1b9385d . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed fap_<clip>_F / fap_<clip>_M.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
MODULE_UNIQUEID: lpr_anim_freeanims
|
||||
MODULE_NAME: Free Animations Pack (SoerGame, Fab ; mannequin UE5)
|
||||
MODULE_AUTHOR: LifePlay Revival (animations by SoerGame)
|
||||
MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival
|
||||
MODULE_DESCRIPTION: Free animation clips from Free Animations Pack (SoerGame, Fab ; mannequin UE5) (CC BY 4.0), retargeted for LifePlay
|
||||
MODULE_REQUIREMENTS: vin_Base
|
||||
MODULE_VERSION: 1.0
|
||||
MODULE_CREATED: 2026-10-07
|
||||
MODULE_LICENSE: CC BY 4.0
|
||||
MODULE_AUTHOR_LINK: https://www.fab.com/sellers/SoerGame
|
||||
MODULE_PRICE: Free
|
||||
MODULE_DEFAULT_ENABLED: true
|
||||
13
ModSources/lpr_anim_freeanims/lpr_anim_freeanims.uplugin
Normal file
13
ModSources/lpr_anim_freeanims/lpr_anim_freeanims.uplugin
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"Description": "Free animation clips from Free Animations Pack (SoerGame, Fab ; mannequin UE5) (CC BY 4.0), retargeted for LifePlay",
|
||||
"Category": "LifePlay Mods",
|
||||
"CreatedBy": "LifePlay Revival (animations by SoerGame)",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"Installed": false,
|
||||
"CreatedByURL": "https://www.fab.com/sellers/SoerGame"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@
|
|||
"pack": "quaternius",
|
||||
"mod": "lpr_anim_quaternius",
|
||||
"version": "1.0",
|
||||
"genere": "2026-10-06T16:30:06",
|
||||
"genere": "2026-10-07T00:33:28",
|
||||
"nombre_animations": 42,
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"animations": [
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "9B579DF07915651F7F712B108C24E86C4626A41056D91B77AC4FA7D0B6A0C35F"
|
||||
"empreinte": "5BF28444BC7AFA908501B19C131EFF428F4C2FCE8C4DBEFBD33BF236C93776FC"
|
||||
},
|
||||
{
|
||||
"cle": "qua_crouch_idle_loop",
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "0DABE423B515ABB6335D03BF58640351F9978B6C1798E69B0DE3E99BBF139821"
|
||||
"empreinte": "6DC0257A33153B969048CA7182204B0567CBECA88645FF8019D68707B2D560FC"
|
||||
},
|
||||
{
|
||||
"cle": "qua_dance_loop",
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "51A7F64FB68A91BE68D2CE8FEDFA635DF66556529605E65D5E9C5DA5E71A987E"
|
||||
"empreinte": "173C48D072A801D11A4230C25CD7C7FA6ACBB4EBF3C2DDC946828BEFF012BCBF"
|
||||
},
|
||||
{
|
||||
"cle": "qua_death01",
|
||||
|
|
@ -309,7 +309,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "5C5C89D9FB3304A71CC3B2CB3018EF2781EACA968CDA0A7F2B6358AEE4CC48D7"
|
||||
"empreinte": "1CF9ACECB34851BB084E9D312002B93BE1E05DFCA5DA661F085B542E6126FD75"
|
||||
},
|
||||
{
|
||||
"cle": "qua_driving_loop",
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "3992745D7BAFD326EBC7150447823724D78E1BC48DCABFA21435BCF3DCD4D2B5"
|
||||
"empreinte": "242E0C0D694B03CE3469D08A24A32F7C68C7510B047BBCD55697EB20715DCD0E"
|
||||
},
|
||||
{
|
||||
"cle": "qua_fixing_kneeling",
|
||||
|
|
@ -463,7 +463,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "0DE675BC7314E651A21431333C791649AAED186DECC116E022F3BEC2D111FF1F"
|
||||
"empreinte": "F3258795367F1D835232BD8AEBC74B8923255F1481794505DCA480FFBA57FC87"
|
||||
},
|
||||
{
|
||||
"cle": "qua_hit_chest",
|
||||
|
|
@ -538,7 +538,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "413F5B0E2BAAD564C4BD39659C8D93378CDC9F70E0C0C29DC8A2EC66026A55E5"
|
||||
"empreinte": "FB7950F28F88AD367751B1AEAFAE3389CEE57CE7883C22796C87F7A8215B4E07"
|
||||
},
|
||||
{
|
||||
"cle": "qua_hit_head",
|
||||
|
|
@ -613,7 +613,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "45F001A3E71833E01151647FF8A7D9EB3ACE6FF6B5D478B3F7ED77F0EAC1FA1A"
|
||||
"empreinte": "176D3A1E9E1E60730390E753233F66D3D880507B19441BA9377A41D33CB6C5CE"
|
||||
},
|
||||
{
|
||||
"cle": "qua_idle_loop",
|
||||
|
|
@ -688,7 +688,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "CDF1F85493A49722295790E16CE55F85535389CBC8F31E38C994FB3D02B0E36B"
|
||||
"empreinte": "C3501B164243DDC82BF77DEFE7F5D404638E24D3822F2BDC782CF2B0A2E465B8"
|
||||
},
|
||||
{
|
||||
"cle": "qua_idle_talking_loop",
|
||||
|
|
@ -763,7 +763,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "2E481B14FFB95D4205B451A75218ECB6932F7E11E7412195BB599C6234839CD8"
|
||||
"empreinte": "61E2C5D612C06F777604212E71FE6640878AA8917B5DFAB75B328BEA7BCBA3FE"
|
||||
},
|
||||
{
|
||||
"cle": "qua_idle_torch_loop",
|
||||
|
|
@ -838,7 +838,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "202DF6A92CF7638F8BD8380C6683F7E6E286C030E831E4D4A43EA06359EFC5B5"
|
||||
"empreinte": "A86E9DE73F030F6534C7FE2D91E5DB894829E0B24E6440925401624031954DFC"
|
||||
},
|
||||
{
|
||||
"cle": "qua_interact",
|
||||
|
|
@ -864,9 +864,9 @@
|
|||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 544,
|
||||
"feminin": 542,
|
||||
"masculin": 531,
|
||||
"total": 1075
|
||||
"total": 1073
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
|
|
@ -913,7 +913,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "6787AD53457B4527F8B8E30B487CCF43524D4DF2DA7C9CBADB5E1102B73AC306"
|
||||
"empreinte": "C649C49CB844ADD53BE596BBF1A3C9E1CEB4D40D0FBA97F16E3166E11E25E5CB"
|
||||
},
|
||||
{
|
||||
"cle": "qua_jog_fwd_loop",
|
||||
|
|
@ -988,7 +988,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "2BF6C3A9BCA1BFE725F30895126F78AD0700411E76198B132F11524E0176A29A"
|
||||
"empreinte": "6914EEB47F8F5D5063B11C64016510AAD1C7A998D8C6E3BB7A64358FAB06EC97"
|
||||
},
|
||||
{
|
||||
"cle": "qua_jump_land",
|
||||
|
|
@ -1063,7 +1063,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "C847CE338880ED7A506CA2CFD6ADC04567D735E061F211D23881A8BF826C99A1"
|
||||
"empreinte": "01EB23A73501C6BC352888D32DD864DBB9DA44EE5F2E49CBE0EBAB1691A064B7"
|
||||
},
|
||||
{
|
||||
"cle": "qua_jump_loop",
|
||||
|
|
@ -1138,7 +1138,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "E74A50B4A340C003209EBB1131EA7369D77D03C7A9A2016A2DA190C16F672674"
|
||||
"empreinte": "A2D7E1D8D497FB4C04B2E337F899DECF52CBDA7A3B786593866A7F8C099166A9"
|
||||
},
|
||||
{
|
||||
"cle": "qua_jump_start",
|
||||
|
|
@ -1165,8 +1165,8 @@
|
|||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 599,
|
||||
"masculin": 591,
|
||||
"total": 1191
|
||||
"masculin": 590,
|
||||
"total": 1189
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
|
|
@ -1218,7 +1218,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "5EB89CFEC6D1329CA7AF815224B653BB7CBFC910EFD27C3817F46E73B4492E57"
|
||||
"empreinte": "B444B1F6443E46E1B3F9B421CEF6BED44745D9DD04E2AA11D33D91F48197D4B2"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pickup_table",
|
||||
|
|
@ -1293,7 +1293,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "A128DEBD8A93E4C2654160F6CD6F53D53C01B72C1D19B50B0406873A3F05D650"
|
||||
"empreinte": "E6481AC8E95369EC2B9590414487136B6BEE77982AF27FD9D40ADDBC7A36E276"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_aim_down",
|
||||
|
|
@ -1368,7 +1368,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "9E23F1BCCDDC168D8DAB59C22E593285A51CDC2962BDF83B03C28A447DA0BECE"
|
||||
"empreinte": "C88CA6AEE605B16B0F842E6C91CB160D5E69019E5B3D820E3CFD6C14AE9D8A1C"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_aim_neutral",
|
||||
|
|
@ -1443,7 +1443,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "984A55AF0B4AB7B5456D237F9F28BE24624C5C8AF8672514045460B1D9FDDA7F"
|
||||
"empreinte": "3BB9CA9A4A5CCBAFB1E46A67BB014B127CB1A7DFC0D79DD0D26BA261A333C7B3"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_aim_up",
|
||||
|
|
@ -1518,7 +1518,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "47FDF5F57483E4831F45C7E8C75BFC5794002B18EC71F35615E0535267964356"
|
||||
"empreinte": "CBD5C628AF2AFDB1801153554CF5CD233662BA792FC689324479BC86A175BA89"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_idle_loop",
|
||||
|
|
@ -1593,7 +1593,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "25AC610F52C00AEF05B6379BF801A004A002B1C1897E2B5E91A2FAAE5BAC6B56"
|
||||
"empreinte": "E4C04AEF22B8EB66983FAEB811379F5B52B4DB9387D6BD563D7A2254BC3DAFFC"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_reload",
|
||||
|
|
@ -1668,7 +1668,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "24AEE8EB7849A51A2FC222845C7847896F5EE48F5F2364FF9BEF05355B39AB47"
|
||||
"empreinte": "AE0E297125CC353DBB6A6E09B2BA0431825EF51F75A291E7519C10F305C64EEA"
|
||||
},
|
||||
{
|
||||
"cle": "qua_pistol_shoot",
|
||||
|
|
@ -1743,7 +1743,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "A5EF75F6DDBC1CBAD89869DC0D022761993EBF88D49E4193421F42CF4991CAC4"
|
||||
"empreinte": "DEA155DB01D349F6A1CD0ABD2E82CDA3C1B1FD645EA5F4F3F1C6DC457BB160C2"
|
||||
},
|
||||
{
|
||||
"cle": "qua_punch_cross",
|
||||
|
|
@ -1818,7 +1818,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "727894040989E2BEF757B2DC50E57447886F5C6CEC90004C4EF5D1F72A5F468E"
|
||||
"empreinte": "24CAA42A311A1C9893A7F180ADC2EC2293AC3C56657237F9D3FB33FC264CBE2F"
|
||||
},
|
||||
{
|
||||
"cle": "qua_punch_jab",
|
||||
|
|
@ -1893,7 +1893,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "0286B49557A3CF43FE37DE88E892E3BF43DDF36157A43049AEB514630F6E3279"
|
||||
"empreinte": "45EA97BFA3E178EBAC27CCD20BBD0C1AB96DF7BC6D4EB2D2673381D0298C0FA7"
|
||||
},
|
||||
{
|
||||
"cle": "qua_push_loop",
|
||||
|
|
@ -1968,7 +1968,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "49D7186234EC47E66A9993FDEA064F4EC4D6A515B2AFA70B7A0E3083B9B87DE2"
|
||||
"empreinte": "EAF3BFF8FB507A6787A716B07FFF39E5CF9D4D373EB2776C15275145A8FDD73A"
|
||||
},
|
||||
{
|
||||
"cle": "qua_roll",
|
||||
|
|
@ -1994,9 +1994,9 @@
|
|||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 631,
|
||||
"masculin": 621,
|
||||
"total": 1252
|
||||
"feminin": 628,
|
||||
"masculin": 618,
|
||||
"total": 1247
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
|
|
@ -2048,7 +2048,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "C86A5D1A1728CB05A99461B1DF39691D07C5011F01C53DDFE7C2E44C9B97733E"
|
||||
"empreinte": "E8789A4A8E24E589F556A4FA297112F5040ECF881F2B25106EFE679A26AEF6B5"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sitting_enter",
|
||||
|
|
@ -2125,7 +2125,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "F71563458C2AE60FECDBEFD8203FEBD96D7318E607FF4B0E6038D4D258E7A98D"
|
||||
"empreinte": "A1ACDCA70C74EF3AAD992ADCA9FB24025002DDE8B357FDE7CBF9B3AEBC731C74"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sitting_exit",
|
||||
|
|
@ -2202,7 +2202,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "6639D68DBFB5833681D25EAFFD8D9043AFBCDA5B75098B100187C4E1FD0BB744"
|
||||
"empreinte": "932A9D24652DAE1F71A6DC21FD669064AE2AC1F62B69751B08AA8D76A387D18A"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sitting_idle_loop",
|
||||
|
|
@ -2278,7 +2278,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "5B70EDA8CEA21B2B83DD66B311149600F248EE9F8243340D70F4AABF26E62FC5"
|
||||
"empreinte": "91261070E46467CF15F78F3A02D4C3E15C4BE91C453AB6AFEB31515F9DDAE2D8"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sitting_talking_loop",
|
||||
|
|
@ -2354,7 +2354,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "F0A4091AE1A72C4ACB9466DFC39D74E5EFACE0BABAC06C12BE428120F69495B8"
|
||||
"empreinte": "2329B5AE17064A1FC996C016256A1571D4B3370E5697A8600170D972A8850EFD"
|
||||
},
|
||||
{
|
||||
"cle": "qua_spell_simple_enter",
|
||||
|
|
@ -2429,7 +2429,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "B3965AAE6955B739C33AA88DC46BE1C8DE44DEB81A2EC5B01C15AA1AF700E680"
|
||||
"empreinte": "00C7EDB5BAECF9E2A08864B6609BCCDFD2B5E2D32843EC2CBDA30CAA2740198B"
|
||||
},
|
||||
{
|
||||
"cle": "qua_spell_simple_exit",
|
||||
|
|
@ -2504,7 +2504,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "45F84F02B7A818D42F18CC0CA4B52EC182A423C8C60CA036AF02092A04F675C2"
|
||||
"empreinte": "C0A8FB24837735C6C9E1886115694B9FDB9F5D835FA7A9F740739AB2FCD9A9CE"
|
||||
},
|
||||
{
|
||||
"cle": "qua_spell_simple_idle_loop",
|
||||
|
|
@ -2579,7 +2579,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "07BAC68BF07A9A6E0E007DEFE06ACF73D3F2245D7D0733EAF0EC321989A5175D"
|
||||
"empreinte": "F755790056282315856B4EDEF1EF497F12C8858D79FBCEB48C966722980DC4C9"
|
||||
},
|
||||
{
|
||||
"cle": "qua_spell_simple_shoot",
|
||||
|
|
@ -2654,7 +2654,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "76FB42C31F2C6B3A01C986DE1C2CE6A887A1E0D6BD1E5E9A8CA6E91FFB7FFE6F"
|
||||
"empreinte": "08C89F2D6EEA08A0CE215152F81520C1A2629E4C39829A0BAE6A92D30962569B"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sprint_loop",
|
||||
|
|
@ -2729,7 +2729,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "9A341C0B31F5EAF0B93E5F9001AB1246D339EF83B4AB3BEB3F17A4A0916F1CDE"
|
||||
"empreinte": "02B94FD1A309E25A9A81FD558B24B9FB503796BAB7C6125351516FE55E7F2C76"
|
||||
},
|
||||
{
|
||||
"cle": "qua_swim_fwd_loop",
|
||||
|
|
@ -2805,7 +2805,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "260E19E1A794EFDE2A8AE788482FE22F03EC3EEC7BE7EE74BC1BDFE01CFCCD65"
|
||||
"empreinte": "C77C54F53AA648222D8B86BD172A1AD1B5A1F42530F7298B389191B0AA94CD30"
|
||||
},
|
||||
{
|
||||
"cle": "qua_swim_idle_loop",
|
||||
|
|
@ -2881,7 +2881,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "181DE0C7C89DE68256F6F592DF1E0BC91765207D7460946715CF8DE385CDD277"
|
||||
"empreinte": "B8D553ABA48541776648BBEB8785C655ADBA135D5C41F4A7B0E3F273386590A4"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sword_attack",
|
||||
|
|
@ -2956,7 +2956,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "412F8748FC0F434155C1312F601C68BACBF025BF20450BE7DBF70524C0BA21D6"
|
||||
"empreinte": "5B3A15EACF396EF46B0BE10BCBE6658A1A8D18E7D762EB49D4376463362834C9"
|
||||
},
|
||||
{
|
||||
"cle": "qua_sword_idle",
|
||||
|
|
@ -3031,7 +3031,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "60227B1417B16DFE5D64C01AF23CFC3731A39B49737FC7B8A6F92AA664F1D18C"
|
||||
"empreinte": "3FFCEF2EF4332F5423035AB1008535E77F67DDD66ED82689582F19F52E74D525"
|
||||
},
|
||||
{
|
||||
"cle": "qua_walk_formal_loop",
|
||||
|
|
@ -3106,7 +3106,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "B6F21793C592498783A5A4FA7BE20889307484B8A86660DBE2537EF3201F13F1"
|
||||
"empreinte": "8FADE11A0F54298B1F62464B329DEA56E75E931A91892C8D76D322D0003134D7"
|
||||
},
|
||||
{
|
||||
"cle": "qua_walk_loop",
|
||||
|
|
@ -3181,7 +3181,7 @@
|
|||
"licence": "CC0 1.0",
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"version": "1.0",
|
||||
"empreinte": "2C83B802CC2E1CE528DACCED02E8DBD6C19D37575694D309DE3615A68E5F2A0B"
|
||||
"empreinte": "90F43B3F0D90DDE9387D90FB05770A118220A88BA0CB109850304211848CB24C"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "HAL/FileManager.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "LPAnimChecks.h"
|
||||
#include "LPAnimMethodSelect.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
#include "LPSkeletonProfile.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
|
|
@ -45,21 +46,45 @@ namespace
|
|||
return Bodies;
|
||||
}
|
||||
|
||||
// Clips de reference du dossier qui ont une copie pour chaque corps (prefixe du corps + nom).
|
||||
TArray<FString> ListClips(const FString& Dir, const TArray<FBody>& Bodies)
|
||||
// Gabarits : "<Corps>:<chemin d'asset>,..." (-Templates). Un corps absent prend <BodyClipDir>/<Prefixe><RefClip>.
|
||||
TMap<FString, FString> ParseTemplates(const FString& Text)
|
||||
{
|
||||
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||||
Registry.ScanPathsSynchronous({ Dir }, true);
|
||||
TArray<FAssetData> Assets;
|
||||
Registry.GetAssetsByPath(FName(*Dir), Assets, false);
|
||||
TSet<FString> Names;
|
||||
for (const FAssetData& Asset : Assets)
|
||||
TMap<FString, FString> Templates;
|
||||
TArray<FString> Items;
|
||||
Text.ParseIntoArray(Items, TEXT(","), true);
|
||||
for (const FString& Item : Items)
|
||||
{
|
||||
if (Asset.AssetClassPath == UAnimSequence::StaticClass()->GetClassPathName())
|
||||
FString Name;
|
||||
FString Path;
|
||||
if (Item.Split(TEXT(":"), &Name, &Path))
|
||||
{
|
||||
Names.Add(Asset.AssetName.ToString());
|
||||
Templates.Add(Name.TrimStartAndEnd(), Path.TrimStartAndEnd());
|
||||
}
|
||||
}
|
||||
return Templates;
|
||||
}
|
||||
|
||||
// Clips de reference du dossier qui ont une copie pour chaque corps (prefixe du corps + nom), copies cherchees dans BodyDir.
|
||||
TArray<FString> ListClips(const FString& Dir, const FString& BodyDir, const TArray<FBody>& Bodies)
|
||||
{
|
||||
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||||
const auto ListNames = [&Registry](const FString& Folder)
|
||||
{
|
||||
Registry.ScanPathsSynchronous({ Folder }, true);
|
||||
TArray<FAssetData> Assets;
|
||||
Registry.GetAssetsByPath(FName(*Folder), Assets, false);
|
||||
TSet<FString> Found;
|
||||
for (const FAssetData& Asset : Assets)
|
||||
{
|
||||
if (Asset.AssetClassPath == UAnimSequence::StaticClass()->GetClassPathName())
|
||||
{
|
||||
Found.Add(Asset.AssetName.ToString());
|
||||
}
|
||||
}
|
||||
return Found;
|
||||
};
|
||||
const TSet<FString> Names = ListNames(Dir);
|
||||
const TSet<FString> BodyNames = BodyDir == Dir ? Names : ListNames(BodyDir);
|
||||
TArray<FString> Clips;
|
||||
for (const FString& Name : Names)
|
||||
{
|
||||
|
|
@ -68,7 +93,7 @@ namespace
|
|||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
bCopy |= Name.StartsWith(Body.Prefix);
|
||||
bAll &= Names.Contains(Body.Prefix + Name);
|
||||
bAll &= BodyNames.Contains(Body.Prefix + Name);
|
||||
}
|
||||
if (!bCopy && bAll)
|
||||
{
|
||||
|
|
@ -123,7 +148,7 @@ namespace
|
|||
|
||||
// Etape 3 : ajustement de la pose T et ses controles.
|
||||
bool CheckTPose(const FLPPose& Reference, const FLPPose& Template, const FBody& Body, const FString& PoseDir, const FString& Out,
|
||||
float SeuilPoseT, float SeuilSymetrie, float SeuilReglage, TArray<FString>& Failures)
|
||||
float SeuilPoseT, float SeuilSymetrie, float SeuilReglage, float SeuilReglageDoigts, TArray<FString>& Failures)
|
||||
{
|
||||
const FString PosePath = FPaths::Combine(PoseDir, FString::Printf(TEXT("retarget_pose_%s.txt"), *Body.Name));
|
||||
FLPTFitResult Fit;
|
||||
|
|
@ -153,6 +178,19 @@ namespace
|
|||
}
|
||||
Report->SetArrayField(TEXT("segments"), Segments);
|
||||
TMap<FName, float> Degrees;
|
||||
// Borne propre aux doigts (AP7) : en pose A, la main du mannequin UE5 est au repos legerement fermee,
|
||||
// celle de Genesis ouverte ; l'ajustement plie les doigts d'environ 30 deg, sans erreur de calque.
|
||||
TSet<FName> Fingers;
|
||||
if (const FLPSkeletonProfile* BodyProfile = LPSkeletonProfile::Find(Template.Profile))
|
||||
{
|
||||
for (const TCHAR* Finger : { TEXT("thumb"), TEXT("index"), TEXT("middle"), TEXT("ring"), TEXT("pinky") })
|
||||
{
|
||||
for (const TCHAR* Side : { TEXT("_l"), TEXT("_r") })
|
||||
{
|
||||
Fingers.Append(BodyProfile->Chain(FString(Finger) + Side));
|
||||
}
|
||||
}
|
||||
}
|
||||
TArray<TSharedPtr<FJsonValue>> Adjusts;
|
||||
for (const FLPTFitAdjust& Adjust : Fit.Adjusts)
|
||||
{
|
||||
|
|
@ -161,9 +199,10 @@ namespace
|
|||
Item->SetStringField(TEXT("os"), Adjust.Bone.ToString());
|
||||
Item->SetNumberField(TEXT("degres"), Adjust.Degrees);
|
||||
Adjusts.Add(MakeShared<FJsonValueObject>(Item));
|
||||
if (Adjust.Degrees > SeuilReglage)
|
||||
const float Bound = Fingers.Contains(Adjust.Bone) ? SeuilReglageDoigts : SeuilReglage;
|
||||
if (Adjust.Degrees > Bound)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : reglage %s %.1f deg (borne %.1f)"), *Body.Name, *Adjust.Bone.ToString(), Adjust.Degrees, SeuilReglage));
|
||||
Failures.Add(FString::Printf(TEXT("%s : reglage %s %.1f deg (borne %.1f)"), *Body.Name, *Adjust.Bone.ToString(), Adjust.Degrees, Bound));
|
||||
}
|
||||
}
|
||||
Report->SetArrayField(TEXT("reglages"), Adjusts);
|
||||
|
|
@ -219,10 +258,13 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
FString PoseDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Measurement"));
|
||||
FString Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("quaternius"));
|
||||
FString ClipsText;
|
||||
FString BodyClipDir;
|
||||
FString TemplatesText;
|
||||
int32 Instants = 3;
|
||||
float SeuilPoseT = 1.f;
|
||||
float SeuilSymetrie = 0.5f;
|
||||
float SeuilReglage = 30.f;
|
||||
float SeuilReglageDoigts = 45.f;
|
||||
float SeuilEval = 1.f;
|
||||
float SeuilTorsion = -1.f;
|
||||
FParse::Value(*Params, TEXT("ClipDir="), ClipDir);
|
||||
|
|
@ -235,7 +277,16 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
FParse::Value(*Params, TEXT("SeuilPoseT="), SeuilPoseT);
|
||||
FParse::Value(*Params, TEXT("SeuilSymetrie="), SeuilSymetrie);
|
||||
FParse::Value(*Params, TEXT("SeuilReglage="), SeuilReglage);
|
||||
FParse::Value(*Params, TEXT("SeuilReglageDoigts="), SeuilReglageDoigts);
|
||||
FParse::Value(*Params, TEXT("SeuilEval="), SeuilEval);
|
||||
// AP7 : clips convertis (prefixes) dans un autre dossier que les clips source ; gabarits explicites par corps.
|
||||
FParse::Value(*Params, TEXT("BodyClipDir="), BodyClipDir);
|
||||
if (BodyClipDir.IsEmpty())
|
||||
{
|
||||
BodyClipDir = ClipDir;
|
||||
}
|
||||
FParse::Value(*Params, TEXT("Templates="), TemplatesText, false);
|
||||
const bool bPoseOnly = FParse::Param(*Params, TEXT("PoseOnly"));
|
||||
// Torsion Bend / Twist : relevee toujours, bloquante seulement si un seuil (degres) est donne.
|
||||
FParse::Value(*Params, TEXT("SeuilTorsion="), SeuilTorsion);
|
||||
// Controles independants (AP4), a chaque image. Seuils etalonnes sur les 43 clips Quaternius (CHG-208) :
|
||||
|
|
@ -273,8 +324,30 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
TArray<FString> Failures;
|
||||
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
|
||||
Summary->SetStringField(TEXT("dossier_clips"), ClipDir);
|
||||
Summary->SetStringField(TEXT("dossier_clips_corps"), BodyClipDir);
|
||||
const TMap<FString, FString> Templates = ParseTemplates(TemplatesText);
|
||||
const auto TemplatePath = [&](const FBody& Body)
|
||||
{
|
||||
const FString* Path = Templates.Find(Body.Name);
|
||||
return Path ? *Path : ClipPath(BodyClipDir, Body.Prefix + RefClip);
|
||||
};
|
||||
TSharedRef<FJsonObject> TemplateReport = MakeShared<FJsonObject>();
|
||||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
TemplateReport->SetStringField(Body.Name, TemplatePath(Body));
|
||||
}
|
||||
Summary->SetObjectField(TEXT("gabarits"), TemplateReport);
|
||||
Summary->SetNumberField(TEXT("instants"), Instants);
|
||||
|
||||
// Methode de transformation (AP7) : avant tout echantillonnage, les calques en dependent.
|
||||
FString MethodError;
|
||||
if (!LPAnimMethodSelect::Apply(Params, ClipPath(ClipDir, RefClip), MethodError))
|
||||
{
|
||||
UE_LOG(LogLPAnimEval, Error, TEXT("ANIMEVAL %s"), *MethodError);
|
||||
return 1;
|
||||
}
|
||||
Summary->SetStringField(TEXT("methode"), LPPoseMeasure::CurrentMethod().Id);
|
||||
|
||||
// Etape 3 : pose T de chaque corps, sur le clip de pose de reference.
|
||||
FLPPose Reference;
|
||||
if (!LPPoseMeasure::Sample(ClipPath(ClipDir, RefClip), 0.f, Reference) || Reference.Profile.IsEmpty())
|
||||
|
|
@ -285,16 +358,34 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
FLPPose Template;
|
||||
if (!LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + RefClip), 0.f, Template) || Template.Profile.IsEmpty())
|
||||
if (!LPPoseMeasure::Sample(TemplatePath(Body), 0.f, Template) || Template.Profile.IsEmpty())
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : clip %s%s absent ou squelette inconnu"), *Body.Name, *Body.Prefix, *RefClip));
|
||||
continue;
|
||||
}
|
||||
CheckTPose(Reference, Template, Body, PoseDir, Out, SeuilPoseT, SeuilSymetrie, SeuilReglage, Failures);
|
||||
CheckTPose(Reference, Template, Body, PoseDir, Out, SeuilPoseT, SeuilSymetrie, SeuilReglage, SeuilReglageDoigts, Failures);
|
||||
}
|
||||
|
||||
// -PoseOnly : arret apres l'etape 3 (les clips convertis n'existent pas encore).
|
||||
if (bPoseOnly)
|
||||
{
|
||||
Summary->SetBoolField(TEXT("pose_seule"), true);
|
||||
TArray<TSharedPtr<FJsonValue>> PoseFailures;
|
||||
for (const FString& Failure : Failures)
|
||||
{
|
||||
PoseFailures.Add(MakeShared<FJsonValueString>(Failure));
|
||||
UE_LOG(LogLPAnimEval, Warning, TEXT("ANIMEVAL echec %s"), *Failure);
|
||||
}
|
||||
Summary->SetArrayField(TEXT("echecs"), PoseFailures);
|
||||
Summary->SetBoolField(TEXT("valide"), Failures.Num() == 0);
|
||||
SaveJson(Summary, FPaths::Combine(Out, TEXT("rapport.json")));
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL resultat pose seule %s : %d echec(s), rapports dans %s"),
|
||||
Failures.Num() == 0 ? TEXT("VALIDE") : TEXT("REFUSE"), Failures.Num(), *Out);
|
||||
return Failures.Num() == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
// Etape 5 : formule et clip converti contre la reference, a chaque instant.
|
||||
TArray<FString> Clips = ListClips(ClipDir, Bodies);
|
||||
TArray<FString> Clips = ListClips(ClipDir, BodyClipDir, Bodies);
|
||||
if (!ClipsText.IsEmpty())
|
||||
{
|
||||
TArray<FString> Wanted;
|
||||
|
|
@ -325,7 +416,7 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
FLPPose RefPose;
|
||||
FLPPose Raw;
|
||||
FLPPose Formula;
|
||||
if (!LPPoseMeasure::Sample(RefPath, Time, RefPose) || !LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + Clip), Time, Raw)
|
||||
if (!LPPoseMeasure::Sample(RefPath, Time, RefPose) || !LPPoseMeasure::Sample(ClipPath(BodyClipDir, Body.Prefix + Clip), Time, Raw)
|
||||
|| !LPPoseMeasure::TransferPose(RefPose, Raw, Formula))
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : %s a %.2f non mesurable"), *Body.Name, *Clip, Fraction));
|
||||
|
|
@ -362,7 +453,7 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
{
|
||||
FLPPose Template, RefT, BodyT;
|
||||
const USkeletalMesh* Mesh = Meshes.Contains(Body.Name) ? LoadObject<USkeletalMesh>(nullptr, *Meshes[Body.Name]) : nullptr;
|
||||
if (LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + RefClip), 0.f, Template) && LPPoseMeasure::TPoses(Reference, Template, RefT, BodyT))
|
||||
if (LPPoseMeasure::Sample(TemplatePath(Body), 0.f, Template) && LPPoseMeasure::TPoses(Reference, Template, RefT, BodyT))
|
||||
{
|
||||
const TArray<FLPCheckCapsule> Trunk = LPAnimChecks::TrunkCapsules(Mesh, BodyT);
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL controles %s : maillage %s, %d capsules de tronc"),
|
||||
|
|
@ -382,7 +473,7 @@ int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
|||
Lines.Add(TEXT("clip,images,sol_cm,sol_os,glissement_cm,glissement_os,genou_cm,genou_os,flexion_deg,flexion_os,saut_deg,saut_os,penetration_cm,penetration_os,verdict"));
|
||||
for (const FString& Clip : Clips)
|
||||
{
|
||||
const FLPClipChecks C = LPAnimChecks::CheckClip(ClipPath(ClipDir, Clip), ClipPath(ClipDir, Body.Prefix + Clip), RefT, BodyT, Trunk);
|
||||
const FLPClipChecks C = LPAnimChecks::CheckClip(ClipPath(ClipDir, Clip), ClipPath(BodyClipDir, Body.Prefix + Clip), RefT, BodyT, Trunk);
|
||||
TArray<FString> Over;
|
||||
bool bReject = !C.Error.IsEmpty();
|
||||
if (bReject) { Over.Add(C.Error); }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Lot AP2 (Docs/Migration/20_Chaine_Animations.md) : etapes 3 et 5 de la chaine des animations, sans fenetre.
|
||||
//
|
||||
// 1. Pose T : ajuste la pose T de chaque corps sur celle de la reference, ecrit retarget_pose_<Corps>.txt
|
||||
// (lu par Tools/RetargetQuaternius.py) et controle l'ajustement : segments ajustes <= SeuilPoseT,
|
||||
// reglages gauche / droite symetriques a SeuilSymetrie degres pres, aucun reglage > SeuilReglage degres.
|
||||
// (lu par Tools/anim/RetargetPack.py) et controle l'ajustement : segments ajustes <= SeuilPoseT,
|
||||
// reglages gauche / droite symetriques a SeuilSymetrie degres pres, aucun reglage > SeuilReglage degres (SeuilReglageDoigts pour les doigts).
|
||||
// 2. Evaluation : pour chaque clip de reference qui a une copie par corps, a Instants instants, compare la
|
||||
// formule (phase B) et le clip converti a la reference ; ecart pire <= SeuilEval.
|
||||
// Rapports : <Out>/rapport_poseT_<Corps>.json, <Out>/eval_<Corps>.csv (colonnes de la tournee de la sandbox),
|
||||
|
|
@ -11,7 +11,12 @@
|
|||
// Usage :
|
||||
// UnrealEditor-Cmd.exe <projet> -run=LPAnimEval [-ClipDir=/Game/Quaternius/UAL] [-RefClip=A_TPose]
|
||||
// [-Bodies=Bridget:B_,Michael:M_] [-Instants=3] [-PoseDir=<Saved/Measurement>] [-Out=<Saved/AnimPipeline/quaternius>]
|
||||
// [-SeuilPoseT=1] [-SeuilSymetrie=0.5] [-SeuilReglage=30] [-SeuilEval=1] [-Clips=Idle_Loop,Walk_Loop]
|
||||
// [-SeuilPoseT=1] [-SeuilSymetrie=0.5] [-SeuilReglage=30] [-SeuilReglageDoigts=45] [-SeuilEval=1] [-Clips=Idle_Loop,Walk_Loop]
|
||||
// [-BodyClipDir=<dossier>] [-Templates=Bridget:<chemin d'asset>,Michael:<chemin d'asset>] [-PoseOnly]
|
||||
// -BodyClipDir : dossier des clips convertis <Prefixe><Clip> (defaut : ClipDir) ; les clips source restent dans ClipDir.
|
||||
// -Templates : clip qui fournit le squelette de chaque corps (defaut : <BodyClipDir>/<Prefixe><RefClip>).
|
||||
// -PoseOnly : s'arrete apres l'etape 3 (retarget_pose_<Corps>.txt, rapport_poseT_<Corps>.json), ecrit rapport.json
|
||||
// avec "pose_seule": true ; sans clips convertis. Code 1 si l'etape 3 echoue, 0 sinon.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
|
|
|||
216
Source/LifePlayEditorTools/LPAnimIdentifyCommandlet.cpp
Normal file
216
Source/LifePlayEditorTools/LPAnimIdentifyCommandlet.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Lot AP7 - voir LPAnimIdentifyCommandlet.h
|
||||
|
||||
#include "LPAnimIdentifyCommandlet.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogLPAnimIdentify, Log, All);
|
||||
|
||||
namespace LPAnimIdentifyImpl
|
||||
{
|
||||
constexpr float Minimum = 0.95f;
|
||||
|
||||
struct FProfileScore
|
||||
{
|
||||
FString Id;
|
||||
int32 Wanted = 0;
|
||||
int32 Present = 0;
|
||||
TArray<FString> Missing;
|
||||
float Score() const { return Wanted > 0 ? static_cast<float>(Present) / Wanted : 0.f; }
|
||||
};
|
||||
|
||||
// Meme regle que LPSkeletonProfile::Identify : tous les os cites par les roles du profil.
|
||||
bool ScoreProfile(const FString& Path, const TArray<FName>& Bones, FProfileScore& Out)
|
||||
{
|
||||
FString Text;
|
||||
TSharedPtr<FJsonObject> Root;
|
||||
const TSharedPtr<FJsonObject>* Roles = nullptr;
|
||||
if (!FFileHelper::LoadFileToString(Text, *Path) || !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Text), Root)
|
||||
|| !Root.IsValid() || !Root->TryGetObjectField(TEXT("roles"), Roles))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Out.Id = Root->GetStringField(TEXT("id"));
|
||||
TSet<FString> Seen;
|
||||
for (const TPair<FString, TSharedPtr<FJsonValue>>& Role : (*Roles)->Values)
|
||||
{
|
||||
TArray<FString> Names;
|
||||
const TArray<TSharedPtr<FJsonValue>>* List = nullptr;
|
||||
if (Role.Value->TryGetArray(List))
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& Item : *List)
|
||||
{
|
||||
Names.Add(Item->AsString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Names.Add(Role.Value->AsString());
|
||||
}
|
||||
for (const FString& Name : Names)
|
||||
{
|
||||
++Out.Wanted;
|
||||
if (Bones.Contains(FName(*Name)))
|
||||
{
|
||||
++Out.Present;
|
||||
}
|
||||
else if (!Seen.Contains(Name))
|
||||
{
|
||||
Seen.Add(Name);
|
||||
Out.Missing.Add(Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return !Out.Id.IsEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
ULPAnimIdentifyCommandlet::ULPAnimIdentifyCommandlet()
|
||||
{
|
||||
IsClient = false;
|
||||
IsServer = false;
|
||||
IsEditor = true;
|
||||
LogToConsole = true;
|
||||
}
|
||||
|
||||
int32 ULPAnimIdentifyCommandlet::Main(const FString& Params)
|
||||
{
|
||||
using namespace LPAnimIdentifyImpl;
|
||||
FString Clip;
|
||||
FString Out;
|
||||
FString Expected;
|
||||
FParse::Value(*Params, TEXT("Clip="), Clip, false);
|
||||
FParse::Value(*Params, TEXT("Out="), Out, false);
|
||||
FParse::Value(*Params, TEXT("Attendu="), Expected, false);
|
||||
if (Clip.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogLPAnimIdentify, Error, TEXT("Usage : -run=LPAnimIdentify -Clip=/Game/<pack>/<clip> [-Out=<f.json>] [-Attendu=<profil>]"));
|
||||
return 1;
|
||||
}
|
||||
if (Out.IsEmpty())
|
||||
{
|
||||
Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("identification.json"));
|
||||
}
|
||||
|
||||
FLPPose Pose;
|
||||
if (!LPPoseMeasure::Sample(Clip, 0.f, Pose))
|
||||
{
|
||||
UE_LOG(LogLPAnimIdentify, Error, TEXT("Clip illisible : %s"), *Clip);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const FString Dir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Tools"), TEXT("anim"), TEXT("profiles"));
|
||||
TArray<FString> Files;
|
||||
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Dir, TEXT("*.json")), true, false);
|
||||
Files.Sort();
|
||||
TArray<FProfileScore> Scores;
|
||||
for (const FString& File : Files)
|
||||
{
|
||||
FProfileScore Score;
|
||||
if (ScoreProfile(FPaths::Combine(Dir, File), Pose.Names, Score))
|
||||
{
|
||||
Scores.Add(MoveTemp(Score));
|
||||
}
|
||||
}
|
||||
// Meilleur score ; a egalite, le profil qui reconnait le plus d'os (le plus specifique).
|
||||
const FProfileScore* Best = nullptr;
|
||||
for (const FProfileScore& Score : Scores)
|
||||
{
|
||||
if (!Best || Score.Score() > Best->Score() + KINDA_SMALL_NUMBER
|
||||
|| (FMath::IsNearlyEqual(Score.Score(), Best->Score()) && Score.Present > Best->Present))
|
||||
{
|
||||
Best = &Score;
|
||||
}
|
||||
}
|
||||
const bool bFound = Best && Best->Score() >= Minimum;
|
||||
const FString Chosen = bFound ? Best->Id : FString();
|
||||
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetStringField(TEXT("clip"), Clip);
|
||||
Report->SetNumberField(TEXT("os"), Pose.Names.Num());
|
||||
Report->SetStringField(TEXT("profil"), Chosen);
|
||||
Report->SetStringField(TEXT("profil_mesure"), Pose.Profile);
|
||||
// Methode de transformation declaree pour ce profil source (Tools/anim/methodes).
|
||||
const FLPTransformMethod* Method = bFound ? LPPoseMeasure::MethodForProfile(Chosen) : nullptr;
|
||||
Report->SetStringField(TEXT("methode"), Method ? Method->Id : FString());
|
||||
Report->SetStringField(TEXT("pose_calques"), Method ? (Method->bPoseT ? TEXT("T") : TEXT("A")) : FString());
|
||||
Report->SetBoolField(TEXT("methode_factice"), Method && Method->bDummy);
|
||||
TArray<TSharedPtr<FJsonValue>> ScoreArray;
|
||||
for (const FProfileScore& Score : Scores)
|
||||
{
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("id"), Score.Id);
|
||||
Item->SetNumberField(TEXT("score_pct"), FMath::RoundToFloat(Score.Score() * 1000.f) / 10.f);
|
||||
Item->SetNumberField(TEXT("os_reconnus"), Score.Present);
|
||||
Item->SetNumberField(TEXT("os_du_profil"), Score.Wanted);
|
||||
TArray<TSharedPtr<FJsonValue>> Missing;
|
||||
for (const FString& Name : Score.Missing)
|
||||
{
|
||||
Missing.Add(MakeShared<FJsonValueString>(Name));
|
||||
}
|
||||
Item->SetArrayField(TEXT("manquants"), Missing);
|
||||
ScoreArray.Add(MakeShared<FJsonValueObject>(Item));
|
||||
UE_LOG(LogLPAnimIdentify, Display, TEXT(" %-16s %5.1f %% (%d / %d), %d manquant(s)"), *Score.Id, Score.Score() * 100.f,
|
||||
Score.Present, Score.Wanted, Score.Missing.Num());
|
||||
}
|
||||
Report->SetArrayField(TEXT("profils"), ScoreArray);
|
||||
TArray<TSharedPtr<FJsonValue>> Bones;
|
||||
for (int32 Bone = 0; Bone < Pose.Names.Num(); ++Bone)
|
||||
{
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("nom"), Pose.Names[Bone].ToString());
|
||||
Item->SetStringField(TEXT("parent"), Pose.Parents.IsValidIndex(Bone) && Pose.Names.IsValidIndex(Pose.Parents[Bone])
|
||||
? Pose.Names[Pose.Parents[Bone]].ToString() : FString());
|
||||
const FVector Rest = Pose.Rest.IsValidIndex(Bone) ? Pose.Rest[Bone].GetLocation() : FVector::ZeroVector;
|
||||
Item->SetArrayField(TEXT("repos"), { MakeShared<FJsonValueNumber>(FMath::RoundToFloat(Rest.X * 10.f) / 10.f),
|
||||
MakeShared<FJsonValueNumber>(FMath::RoundToFloat(Rest.Y * 10.f) / 10.f), MakeShared<FJsonValueNumber>(FMath::RoundToFloat(Rest.Z * 10.f) / 10.f) });
|
||||
Bones.Add(MakeShared<FJsonValueObject>(Item));
|
||||
}
|
||||
Report->SetArrayField(TEXT("squelette"), Bones);
|
||||
|
||||
TArray<FString> Failures;
|
||||
if (!bFound)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("squelette inconnu : meilleur profil %s a %.1f %% (< 95 %%)"),
|
||||
Best ? *Best->Id : TEXT("aucun"), Best ? Best->Score() * 100.f : 0.f));
|
||||
}
|
||||
else if (Pose.Profile != Chosen)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("le module de mesure identifie %s, l'identification retient %s"), *Pose.Profile, *Chosen));
|
||||
}
|
||||
if (bFound && !Method)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("aucune methode de transformation pour le profil %s (Tools/anim/methodes)"), *Chosen));
|
||||
}
|
||||
if (!Expected.IsEmpty() && Expected != Chosen)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("profil attendu %s, retenu %s"), *Expected, *Chosen));
|
||||
}
|
||||
TArray<TSharedPtr<FJsonValue>> Errors;
|
||||
for (const FString& Failure : Failures)
|
||||
{
|
||||
Errors.Add(MakeShared<FJsonValueString>(Failure));
|
||||
UE_LOG(LogLPAnimIdentify, Error, TEXT("%s"), *Failure);
|
||||
}
|
||||
Report->SetArrayField(TEXT("echecs"), Errors);
|
||||
Report->SetBoolField(TEXT("valide"), Failures.Num() == 0);
|
||||
|
||||
FString Text;
|
||||
FJsonSerializer::Serialize(Report, TJsonWriterFactory<>::Create(&Text));
|
||||
IFileManager::Get().MakeDirectory(*FPaths::GetPath(Out), true);
|
||||
if (!FFileHelper::SaveStringToFile(Text, *Out, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogLPAnimIdentify, Error, TEXT("ecriture impossible : %s"), *Out);
|
||||
return 1;
|
||||
}
|
||||
UE_LOG(LogLPAnimIdentify, Warning, TEXT("IDENTIFICATION %s : profil %s (mesure %s), methode %s, %d os -> %s"), *Clip,
|
||||
Chosen.IsEmpty() ? TEXT("aucun") : *Chosen, *Pose.Profile, Method ? *Method->Id : TEXT("aucune"), Pose.Names.Num(), *Out);
|
||||
return Failures.Num() > 0 ? 1 : 0;
|
||||
}
|
||||
28
Source/LifePlayEditorTools/LPAnimIdentifyCommandlet.h
Normal file
28
Source/LifePlayEditorTools/LPAnimIdentifyCommandlet.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Lot AP7 (Docs/Migration/20_Chaine_Animations.md, section 3, etape 2) : identification du squelette d'un pack.
|
||||
//
|
||||
// Echantillonne un clip du pack (LPPoseMeasure::Sample) et compare ses os a chaque profil de Tools/anim/profiles :
|
||||
// part des os du profil presents dans le squelette, os manquants. Le profil retenu doit atteindre 95 % ; a egalite,
|
||||
// le plus complet (le plus d'os reconnus) l'emporte, et il doit etre celui qu'identifie le module de mesure
|
||||
// (FLPPose::Profile), sinon la mesure et le transfert utiliseraient une autre table que celle annoncee.
|
||||
// Ecrit aussi la liste des os (nom, parent, position de repos en espace composant) pour ecrire un profil.
|
||||
//
|
||||
// Usage :
|
||||
// UnrealEditor-Cmd.exe <projet> -run=LPAnimIdentify -Clip=/Game/<pack>/<clip> [-Out=<fichier.json>]
|
||||
// [-Attendu=<id de profil>] (code 1 si le profil retenu differe)
|
||||
// Code 0 si un profil est retenu (et egal a -Attendu s'il est donne), 1 sinon.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "LPAnimIdentifyCommandlet.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class ULPAnimIdentifyCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULPAnimIdentifyCommandlet();
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
48
Source/LifePlayEditorTools/LPAnimMethodSelect.h
Normal file
48
Source/LifePlayEditorTools/LPAnimMethodSelect.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Lot AP7 (Docs/Migration/20_Chaine_Animations.md) : choix de la methode de transformation par les commandlets.
|
||||
//
|
||||
// -Methode=<id> impose une methode de Tools/anim/methodes ; -Methode=auto (ou absent) la deduit du profil du
|
||||
// squelette source (clip de reference), par LPPoseMeasure::MethodForProfile. A appeler avant tout autre
|
||||
// echantillonnage : les calques (pose A ou T) sont construits par LPPoseMeasure::Sample.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
|
||||
namespace LPAnimMethodSelect
|
||||
{
|
||||
// false (avec la cause dans Error) si la methode est inconnue, factice, ou introuvable pour le profil source.
|
||||
inline bool Apply(const FString& Params, const FString& RefClipPath, FString& Error)
|
||||
{
|
||||
FString Wanted;
|
||||
FParse::Value(*Params, TEXT("Methode="), Wanted);
|
||||
if (Wanted.IsEmpty() || Wanted.Equals(TEXT("auto"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
LPPoseMeasure::SetMethod(FString());
|
||||
FLPPose Probe;
|
||||
if (!LPPoseMeasure::Sample(RefClipPath, 0.f, Probe) || Probe.Profile.IsEmpty())
|
||||
{
|
||||
Error = FString::Printf(TEXT("methode auto : squelette source non identifie (%s)"), *RefClipPath);
|
||||
return false;
|
||||
}
|
||||
const FLPTransformMethod* Found = LPPoseMeasure::MethodForProfile(Probe.Profile);
|
||||
if (!Found)
|
||||
{
|
||||
Error = FString::Printf(TEXT("methode auto : aucune methode pour le profil %s"), *Probe.Profile);
|
||||
return false;
|
||||
}
|
||||
Wanted = Found->Id;
|
||||
}
|
||||
if (!LPPoseMeasure::SetMethod(Wanted))
|
||||
{
|
||||
Error = FString::Printf(TEXT("methode inconnue : %s"), *Wanted);
|
||||
return false;
|
||||
}
|
||||
if (LPPoseMeasure::CurrentMethod().bDummy)
|
||||
{
|
||||
Error = FString::Printf(TEXT("methode %s declaree mais non implementee"), *Wanted);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "LPAnimMethodSelect.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
#include "LPSkeletonProfile.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
|
|
@ -53,6 +54,24 @@ namespace
|
|||
return FString::Printf(TEXT("%s/%s.%s"), *Dir, *Name, *Name);
|
||||
}
|
||||
|
||||
// Gabarits : "<Corps>:<chemin d'asset>,..." (-Templates). Un corps absent prend <BodyClipDir>/<Prefixe><RefClip>.
|
||||
TMap<FString, FString> ParseTemplates(const FString& Text)
|
||||
{
|
||||
TMap<FString, FString> Templates;
|
||||
TArray<FString> Items;
|
||||
Text.ParseIntoArray(Items, TEXT(","), true);
|
||||
for (const FString& Item : Items)
|
||||
{
|
||||
FString Name;
|
||||
FString Path;
|
||||
if (Item.Split(TEXT(":"), &Name, &Path))
|
||||
{
|
||||
Templates.Add(Name.TrimStartAndEnd(), Path.TrimStartAndEnd());
|
||||
}
|
||||
}
|
||||
return Templates;
|
||||
}
|
||||
|
||||
TArray<FTwistPair> PairsFor(const FLPPose& BodyT)
|
||||
{
|
||||
TArray<FTwistPair> Pairs;
|
||||
|
|
@ -258,9 +277,32 @@ int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
|||
FParse::Value(*Params, TEXT("Bodies="), BodiesText, false);
|
||||
FParse::Value(*Params, TEXT("Clips="), ClipsText, false);
|
||||
FParse::Value(*Params, TEXT("Out="), Out);
|
||||
// AP7 : clips convertis (prefixes) dans un autre dossier que les clips source ; gabarits explicites par corps.
|
||||
FString BodyClipDir;
|
||||
FString TemplatesText;
|
||||
FParse::Value(*Params, TEXT("BodyClipDir="), BodyClipDir);
|
||||
if (BodyClipDir.IsEmpty())
|
||||
{
|
||||
BodyClipDir = ClipDir;
|
||||
}
|
||||
FParse::Value(*Params, TEXT("Templates="), TemplatesText, false);
|
||||
const TMap<FString, FString> Templates = ParseTemplates(TemplatesText);
|
||||
const auto TemplatePath = [&](const FString& BodyName, const FString& Prefix)
|
||||
{
|
||||
const FString* Path = Templates.Find(BodyName);
|
||||
return Path ? *Path : ClipPath(BodyClipDir, Prefix + RefClip);
|
||||
};
|
||||
const bool bDryRun = FParse::Param(*Params, TEXT("DryRun"));
|
||||
IFileManager::Get().MakeDirectory(*Out, true);
|
||||
|
||||
// Methode de transformation (AP7) : le calque de reference (pose A ou T) en depend.
|
||||
FString MethodError;
|
||||
if (!LPAnimMethodSelect::Apply(Params, ClipPath(ClipDir, RefClip), MethodError))
|
||||
{
|
||||
UE_LOG(LogLPAnimTwist, Error, TEXT("TORSION %s"), *MethodError);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FLPPose Reference;
|
||||
if (!LPPoseMeasure::Sample(ClipPath(ClipDir, RefClip), 0.f, Reference))
|
||||
{
|
||||
|
|
@ -268,9 +310,9 @@ int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
|||
return 1;
|
||||
}
|
||||
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||||
Registry.ScanPathsSynchronous({ ClipDir }, true);
|
||||
Registry.ScanPathsSynchronous({ BodyClipDir }, true);
|
||||
TArray<FAssetData> Assets;
|
||||
Registry.GetAssetsByPath(FName(*ClipDir), Assets, false);
|
||||
Registry.GetAssetsByPath(FName(*BodyClipDir), Assets, false);
|
||||
TArray<FString> Wanted;
|
||||
ClipsText.ParseIntoArray(Wanted, TEXT(","), true);
|
||||
|
||||
|
|
@ -279,6 +321,7 @@ int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
|||
int32 Failures = 0;
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetBoolField(TEXT("essai_a_blanc"), bDryRun);
|
||||
Report->SetStringField(TEXT("dossier_clips_corps"), BodyClipDir);
|
||||
TArray<TSharedPtr<FJsonValue>> BodyReports;
|
||||
for (const FString& Item : BodyItems)
|
||||
{
|
||||
|
|
@ -291,7 +334,7 @@ int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
|||
FLPPose Template;
|
||||
FLPPose RefT;
|
||||
FLPPose BodyT;
|
||||
if (!LPPoseMeasure::Sample(ClipPath(ClipDir, Prefix + RefClip), 0.f, Template)
|
||||
if (!LPPoseMeasure::Sample(TemplatePath(BodyName, Prefix), 0.f, Template)
|
||||
|| !LPPoseMeasure::TPoses(Reference, Template, RefT, BodyT))
|
||||
{
|
||||
UE_LOG(LogLPAnimTwist, Error, TEXT("TORSION %s : pose T impossible"), *BodyName);
|
||||
|
|
@ -310,7 +353,7 @@ int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
|||
{
|
||||
continue;
|
||||
}
|
||||
UAnimSequence* Sequence = LoadObject<UAnimSequence>(nullptr, *ClipPath(ClipDir, Name));
|
||||
UAnimSequence* Sequence = LoadObject<UAnimSequence>(nullptr, *ClipPath(BodyClipDir, Name));
|
||||
if (!Sequence)
|
||||
{
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
// Usage :
|
||||
// UnrealEditor-Cmd.exe <projet> -run=LPAnimTwist [-ClipDir=/Game/Quaternius/UAL] [-RefClip=A_TPose]
|
||||
// [-Bodies=Bridget:B_,Michael:M_] [-Clips=Idle_Loop,Walk_Loop] [-DryRun] [-Out=<Saved/AnimPipeline/quaternius>]
|
||||
// [-BodyClipDir=<dossier>] [-Templates=Bridget:<chemin d'asset>,Michael:<chemin d'asset>]
|
||||
// -BodyClipDir : dossier des clips convertis <Prefixe><Clip> (defaut : ClipDir) ; les clips source restent dans ClipDir.
|
||||
// -Templates : clip qui fournit le squelette de chaque corps (defaut : <BodyClipDir>/<Prefixe><RefClip>).
|
||||
// Rapport : <Out>/rapport_torsion.json. Code de sortie 0 si chaque clip est traite et conserve les autres os.
|
||||
|
||||
#pragma once
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
Sources : ModSources\<Nom>\ (<Nom>.uplugin, Content\, Module\ ; voir Tools/ModSource.ps1).
|
||||
1. controles : plugin et module (MODULE_UNIQUEID = <Nom>), version de reference Releases\<Version>\Windows, aucun
|
||||
autre plugin dans Mods\ ;
|
||||
1b. -Retarget bake|rigs : recopie les clips Quaternius sur Bridget et Michael (Tools/RetargetQuaternius.py
|
||||
dans UnrealEditor.exe, Slate requis). L'editeur doit etre ferme. -RetargetOnly s'arrete avant la cuisson ;
|
||||
0. -Anim <pack> : chaine complete d'animations du pack (Tools\anim\AnimPipeline.ps1 -Pack <pack>, lot AP7) ;
|
||||
-From <etape> et -SansCuisson lui sont transmis ; rien d'autre n'est execute ;
|
||||
1b. -Retarget bake|rigs : recopie les clips Quaternius sur Bridget et Michael (Tools/anim/RetargetPack.py
|
||||
dans UnrealEditor.exe, Slate requis ; -RetargetConfig <json> : copie vers Saved\anim_retarget.json, methode et pack). L'editeur doit etre ferme. -RetargetOnly s'arrete avant la cuisson ;
|
||||
1c. -Target sandbox|game ecrit Saved\Config\Windows\LPAnimTarget.ini. -Open lance cette cible
|
||||
(sandbox : chambre d'essai ; game : partie, carte par defaut) et ne cuit pas ;
|
||||
2. montage du mod dans Mods\<Nom>, cuisson DLC contre la version de reference (BuildCookRun -dlcname
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake -RetargetOnly
|
||||
.\Tools\BuildMod.ps1 -Anim freeanims
|
||||
.\Tools\BuildMod.ps1 -Target sandbox -Open
|
||||
.\Tools\BuildMod.ps1 -Target game -Open
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Install D:\Game\LifePlay_Build_5.5_H3d\Windows
|
||||
|
|
@ -32,9 +35,13 @@ param(
|
|||
[string] $Out = 'D:\Game\LifePlay_Publication\Mods',
|
||||
[string] $Install = '',
|
||||
[string] $Retarget = '',
|
||||
[string] $RetargetConfig = '',
|
||||
[switch] $RetargetOnly,
|
||||
[string] $Target = '',
|
||||
[switch] $Open
|
||||
[switch] $Open,
|
||||
[string] $Anim = '',
|
||||
[int] $From = 0,
|
||||
[switch] $SansCuisson
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
|
@ -43,21 +50,58 @@ $Root = Split-Path $PSScriptRoot -Parent
|
|||
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine'
|
||||
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
|
||||
|
||||
function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode) {
|
||||
# AP7 : chaine d'animations d'un pack.
|
||||
if ($Anim) {
|
||||
$pipe = @{ Pack = $Anim }
|
||||
if ($PSBoundParameters.ContainsKey('From')) { $pipe.From = $From }
|
||||
if ($SansCuisson) { $pipe.SansCuisson = $true }
|
||||
& (Join-Path $PSScriptRoot 'anim\AnimPipeline.ps1') @pipe
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# AP7 : un resultat ou un echec de conversion n'est retenu que s'il porte le jeton de ce lancement
|
||||
# (un ancien journal, ex. LifePlay-backup-<date>.log relu depuis l'octet 0, n'a jamais ce jeton).
|
||||
function Find-RetargetLine([string] $Text, [string] $Mode, [string] $Jeton) {
|
||||
$tok = [regex]::Escape($Jeton)
|
||||
$success = if ($Mode -eq 'rigs') { "RETARGET rigs prets jeton $tok(?![0-9A-Za-z])" } else { "RETARGET bridget \d+ michael \d+ deplaces \d+[^\r\n]*? jeton $tok(?![0-9A-Za-z])" }
|
||||
$ok = [regex]::Match($Text, $success)
|
||||
$bad = [regex]::Match($Text, "RETARGET echec[^\r\n]*? jeton $tok(?![0-9A-Za-z])")
|
||||
[pscustomobject]@{
|
||||
Hit = if ($ok.Success) { $ok.Value.Trim() } else { $null }
|
||||
Failure = if ($bad.Success) { $bad.Value.Trim() } else { $null }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode, [string] $ConfigFile = '') {
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la conversion" }
|
||||
$stepFile = Join-Path $ProjectRoot 'Saved\anim_to_genesis_step.txt'
|
||||
New-Item -ItemType Directory -Force (Split-Path $stepFile) | Out-Null
|
||||
[IO.File]::WriteAllText($stepFile, ($Mode.Trim().ToLower() + "`r`n"), (New-Object Text.UTF8Encoding $false))
|
||||
$configTarget = Join-Path $ProjectRoot 'Saved\anim_retarget.json'
|
||||
New-Item -ItemType Directory -Force (Split-Path $configTarget) | Out-Null
|
||||
$jeton = [guid]::NewGuid().ToString('N').Substring(0, 12)
|
||||
if ($ConfigFile) {
|
||||
if (-not (Test-Path $ConfigFile)) { throw "-RetargetConfig : fichier introuvable $ConfigFile" }
|
||||
$cfg = Get-Content -LiteralPath $ConfigFile -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
$cfg | Add-Member -NotePropertyName jeton -NotePropertyValue $jeton -Force
|
||||
[IO.File]::WriteAllText($configTarget, (($cfg | ConvertTo-Json -Depth 20) + "`r`n"), (New-Object Text.UTF8Encoding $false))
|
||||
}
|
||||
else {
|
||||
[IO.File]::WriteAllText($configTarget, ('{"mode": "' + $Mode.Trim().ToLower() + '", "jeton": "' + $jeton + '"}' + "`r`n"), (New-Object Text.UTF8Encoding $false))
|
||||
}
|
||||
$effective = Get-Content -LiteralPath $configTarget -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
$methode = if ($effective.methode) { [string] $effective.methode } else { 'ue4' }
|
||||
$clipsDir = if ($effective.clips_dir) { [string] $effective.clips_dir } else { '/Game/Quaternius/UAL' }
|
||||
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
|
||||
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
|
||||
$script = Join-Path $ProjectRoot 'Tools\RetargetQuaternius.py'
|
||||
$script = Join-Path $ProjectRoot 'Tools\anim\RetargetPack.py'
|
||||
$logDir = Join-Path $ProjectRoot 'Saved\Logs'
|
||||
$success = if ($Mode -eq 'rigs') { 'RETARGET rigs prets' } else { 'RETARGET bridget \d+ michael \d+ deplaces \d+' }
|
||||
$before = @{}
|
||||
Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue | ForEach-Object { $before[$_.FullName] = $_.Length }
|
||||
Write-Host "Conversion des clips Quaternius vers Bridget et Michael ($Mode)"
|
||||
$proc = Start-Process -FilePath $editor -ArgumentList @($project, "-ExecutePythonScript=$script", '-nosplash', '-unattended') -WorkingDirectory $ProjectRoot -PassThru
|
||||
# Journaux relus en entier s'ils ont change depuis le lancement : le jeton suffit a ecarter les lignes d'une ancienne
|
||||
# cuisson. (AP7 : l'ancienne lecture a partir de la taille d'avant sautait le nouveau LifePlay.log, recree plus petit
|
||||
# par l'editeur au demarrage, et ne voyait jamais la ligne de resultat.)
|
||||
$launched = Get-Date
|
||||
Write-Host "Conversion des clips de $clipsDir vers Bridget et Michael (methode $methode, $Mode)"
|
||||
# Chemins entre guillemets : le dossier du projet contient une espace (AP7 : sans guillemets, l'editeur
|
||||
# recevait un chemin de projet coupe et se fermait sans journal).
|
||||
$proc = Start-Process -FilePath $editor -ArgumentList "`"$project`" -ExecutePythonScript=`"$script`" -nosplash -unattended" -WorkingDirectory $ProjectRoot -PassThru
|
||||
$deadline = (Get-Date).AddMinutes(25)
|
||||
$hit = $null
|
||||
$failure = $null
|
||||
|
|
@ -65,23 +109,19 @@ function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode) {
|
|||
Start-Sleep -Seconds 5
|
||||
$logs = @(Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue)
|
||||
foreach ($file in $logs) {
|
||||
$offset = 0
|
||||
if ($before.ContainsKey($file.FullName)) { $offset = $before[$file.FullName] }
|
||||
if ($file.Length -le $offset) { continue }
|
||||
if ($file.LastWriteTime -lt $launched) { continue }
|
||||
try {
|
||||
$stream = [IO.File]::Open($file.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
||||
}
|
||||
catch [System.IO.IOException] { continue }
|
||||
try {
|
||||
$stream.Seek($offset, [IO.SeekOrigin]::Begin) | Out-Null
|
||||
$reader = New-Object IO.StreamReader($stream)
|
||||
$text = $reader.ReadToEnd()
|
||||
}
|
||||
finally { $stream.Close() }
|
||||
$ok = [regex]::Match($text, $success)
|
||||
if ($ok.Success) { $hit = $ok.Value }
|
||||
$bad = [regex]::Match($text, 'chaine impossible[^\r\n]*|RETARGET deplacement rate[^\r\n]*|etape inconnue[^\r\n]*|LogPython: Error:[^\r\n]*')
|
||||
if ($bad.Success) { $failure = $bad.Value.Trim() }
|
||||
$found = Find-RetargetLine $text $Mode $jeton
|
||||
if ($found.Hit) { $hit = $found.Hit }
|
||||
if ($found.Failure) { $failure = $found.Failure }
|
||||
}
|
||||
if ($hit) { break }
|
||||
$editorGone = -not (Get-Process UnrealEditor -ErrorAction SilentlyContinue)
|
||||
|
|
@ -89,8 +129,10 @@ function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode) {
|
|||
}
|
||||
if ($failure -and -not $hit) { throw "Conversion en echec : $failure" }
|
||||
if (-not $hit) { throw "Conversion sans ligne RETARGET : voir Saved\Logs\LifePlay.log" }
|
||||
$quiet = (Get-Date).AddMinutes(2)
|
||||
while ((Get-Date) -lt $quiet -and (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 2 }
|
||||
# L'editeur se ferme seul apres la conversion (RetargetPack.py), mais l'enregistrement des assets peut prendre
|
||||
# plusieurs minutes (AP7 : plus de 2 min pour 84 clips) : attendre jusqu'a 10 min que plus aucun editeur ne tourne.
|
||||
$quiet = (Get-Date).AddMinutes(10)
|
||||
while ((Get-Date) -lt $quiet -and @(Get-Process UnrealEditor -ErrorAction SilentlyContinue).Count -gt 0) { Start-Sleep -Seconds 2 }
|
||||
$still = @(Get-Process UnrealEditor -ErrorAction SilentlyContinue)
|
||||
if ($still) { throw "Conversion terminee ($hit) mais UnrealEditor est encore ouvert : le fermer avant la cuisson" }
|
||||
Write-Host " $hit"
|
||||
|
|
@ -165,7 +207,7 @@ if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les reme
|
|||
if ($RetargetOnly -and -not $Retarget) { $Retarget = 'bake' }
|
||||
if ($Retarget -and $Retarget -notin 'bake', 'rigs') { throw "-Retarget vaut bake ou rigs (recu : $Retarget)" }
|
||||
if ($Target) { Write-AnimTarget $Root $Target }
|
||||
if ($Retarget) { Invoke-AnimToGenesis $Root $Retarget }
|
||||
if ($Retarget) { Invoke-AnimToGenesis $Root $Retarget $RetargetConfig }
|
||||
if ($Open) {
|
||||
Open-AnimTarget $Root $Target
|
||||
exit 0
|
||||
|
|
|
|||
563
Tools/anim/AnimPipeline.ps1
Normal file
563
Tools/anim/AnimPipeline.ps1
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Pilote de la chaine d'animations d'un pack, de la source au mod livre (lot AP7, Docs/Migration/20_Chaine_Animations.md, section 3).
|
||||
|
||||
.DESCRIPTION
|
||||
-Pack <nom> : dossier AnimSources\<nom> ; la chaine est decrite par le bloc "chaine" de AnimSources\<nom>\pack.json.
|
||||
Huit etapes, un controle bloquant a chacune, arret au premier controle en echec :
|
||||
0 inventaire pack.json complet, licence / credit / auteur renseignes (refus de "A COMPLETER"), sources trouvees,
|
||||
empreintes SHA-256 des .uasset de la source
|
||||
1 import glb : script d'import (saute si clips presents et glb inchange) ; uasset : rien. Clips > 0
|
||||
2 identif. LPAnimIdentify sur le clip de reference ; valide, methode non factice ; la methode est retenue
|
||||
3 calque LPAnimEval -PoseOnly (pose T des corps) ; rapport.json valide
|
||||
4 conversion BuildMod -Retarget bake (cmd /c start /wait) ; ligne RETARGET bridget n michael n (n = clips) ;
|
||||
puis LPAnimTwist ; rapport_torsion.json valide
|
||||
5 evaluation LPAnimEval complet (un clip rejete bloque), rapport.html, puis sandbox (vignettes et planches)
|
||||
6 catalogue copie des clips dans le mod (DeliverAnims.ps1, ecrit livraison.json), LPAnimCatalog (fiches),
|
||||
Catalogue.py (DeliverAnims.ps1 -CatalogueOnly). La copie precede les fiches : le controle exige
|
||||
fiches_calculees.json plus recent que livraison.json
|
||||
7 livraison cuisson DLC du mod (BuildMod.ps1 -Name <mod>) ; sans version de reference Releases\<version>\Windows :
|
||||
avertissement, sans echec ; -SansCuisson la saute
|
||||
Rapports : <travail>\etapes\<n>_<nom>.json (debut, fin, duree, commandes, code, controles, verdict) et
|
||||
<travail>\etat.json (avancement, methode retenue, empreintes). -From <n> reprend a l'etape n (etat.json lu ;
|
||||
etapes precedentes non rejouees) ; -To <n> s'arrete apres l'etape n. Un passage complet (sans -From) deplace
|
||||
l'ancien contenu de <travail> dans <travail>\precedent_<date> (jamais supprime).
|
||||
-DryRun : affiche le plan (commandes exactes, dans l'ordre), ne lance ni ne cree rien ; l'etape 0 y est evaluee
|
||||
pour de bon et ses echecs sont montres comme "echec prevu" sans arreter le plan.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack quaternius -DryRun
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims -From 5 -SansCuisson
|
||||
.\Tools\BuildMod.ps1 -Anim freeanims
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string] $Pack,
|
||||
[ValidateRange(0, 7)][int] $From = 0,
|
||||
[ValidateRange(0, 7)][int] $To = 7,
|
||||
[switch] $DryRun,
|
||||
[switch] $SansCuisson
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$Pwsh = (Get-Process -Id $PID).Path
|
||||
$Utf8 = New-Object Text.UTF8Encoding $false
|
||||
$StepNames = 'inventaire', 'import', 'identification', 'calque', 'conversion', 'evaluation', 'catalogue', 'livraison'
|
||||
|
||||
# ---------------------------------------------------------------- etat du passage
|
||||
$script:DryRun = [bool] $DryRun
|
||||
$script:Cfg = $null # pack.json
|
||||
$script:Ch = $null # bloc chaine
|
||||
$script:Work = '' # dossier de travail (absolu, barres obliques)
|
||||
$script:Method = '' # methode retenue (etape 2) ; vide tant qu'inconnue
|
||||
$script:Clips = @() # clips source (noms), hors B_/M_ et exclus
|
||||
$script:Etat = [ordered]@{ pack = $Pack; etapes = [ordered]@{} }
|
||||
$script:PrevEtat = $null
|
||||
$script:St = $null # etape en cours
|
||||
$script:Planned = @() # echecs prevus (essai a blanc)
|
||||
|
||||
function Show-Rel([string] $Path) {
|
||||
$p = $Path -replace '/', '\'
|
||||
if ($p.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) { return $p.Substring($Root.Length).TrimStart('\') }
|
||||
$p
|
||||
}
|
||||
function Add-Cmd([string] $Text) {
|
||||
$script:St.Cmds += $Text
|
||||
Write-Host " > $Text"
|
||||
}
|
||||
function Add-Check([string] $Name, [bool] $Ok, [string] $Detail = '') {
|
||||
$script:St.Checks += [ordered]@{ nom = $Name; ok = $Ok; detail = $Detail }
|
||||
if ($Ok) { Write-Host " [ok] $Name$(if ($Detail) { " : $Detail" })" }
|
||||
elseif ($script:DryRun) { Write-Host " [ECHEC PREVU] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Yellow; $script:Planned += "etape $($script:St.N) : $Name$(if ($Detail) { " : $Detail" })" }
|
||||
else { Write-Host " [ECHEC] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Red }
|
||||
}
|
||||
function Add-Warn([string] $Text) {
|
||||
$script:St.Warns += $Text
|
||||
Write-Host " [avertissement] $Text" -ForegroundColor Yellow
|
||||
}
|
||||
function Read-Json([string] $Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
||||
try { Get-Content -LiteralPath $Path -Raw -Encoding utf8 | ConvertFrom-Json } catch { $null }
|
||||
}
|
||||
function Write-Text([string] $Path, [string] $Text) {
|
||||
New-Item -ItemType Directory -Force (Split-Path $Path -Parent) | Out-Null
|
||||
[IO.File]::WriteAllText($Path, $Text, $Utf8)
|
||||
}
|
||||
function ConvertTo-Disk([string] $GamePath) {
|
||||
if ($GamePath -notmatch '^/Game/(.+)$') { throw "chemin Unreal invalide (attendu /Game/...) : $GamePath" }
|
||||
Join-Path $Root ('Content\' + ($Matches[1] -replace '/', '\'))
|
||||
}
|
||||
function Get-SourceClips {
|
||||
$dir = ConvertTo-Disk $script:Ch.source.clips
|
||||
if (-not (Test-Path $dir)) { return @() }
|
||||
$excl = @($script:Cfg.exclus | Where-Object { $_ })
|
||||
# Hors maillage et squelette importes avec les clips (glb : <maillage>, <maillage>_Skeleton, <maillage>_PhysicsAsset).
|
||||
$mesh = ([string] $script:Ch.source.maillage -replace '^.*/', '' -replace '\..*$', '')
|
||||
@(Get-ChildItem $dir -File -Filter '*.uasset' | ForEach-Object { $_.BaseName } |
|
||||
Where-Object { $_ -notlike 'B_*' -and $_ -notlike 'M_*' -and $_ -notin $excl -and -not ($mesh -and $_ -like "$mesh*") -and $_ -notlike '*_Skeleton' -and $_ -notlike '*_PhysicsAsset' } | Sort-Object)
|
||||
}
|
||||
function Test-Placeholder($Value) {
|
||||
$s = [string] $Value
|
||||
(-not $s.Trim()) -or ($s -match 'A COMPLETER')
|
||||
}
|
||||
function Test-EditorClosed {
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
Add-Check 'editeur ferme' ($running.Count -eq 0) $(if ($running) { "UnrealEditor ouvert (pid $($running[0].Id))" } else { '' })
|
||||
$running.Count -eq 0
|
||||
}
|
||||
function Get-MethodArg { if ($script:Method) { $script:Method } elseif ($script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Ch.methode } else { '<methode-etape-2>' } }
|
||||
function Get-WorkRel { Show-Rel $script:Work }
|
||||
|
||||
# Commandlet via Tools\RunCommandlet.ps1 ; rend le code de sortie.
|
||||
function Invoke-Commandlet([string] $Commandlet, [string] $CmdArgs) {
|
||||
Add-Cmd "Tools\RunCommandlet.ps1 -Lot AP7 -Commandlet $Commandlet -Args '$CmdArgs'"
|
||||
if ($script:DryRun) { return 0 }
|
||||
$global:LASTEXITCODE = 0
|
||||
& (Join-Path $Root 'Tools\RunCommandlet.ps1') -Lot AP7 -Commandlet $Commandlet -Args $CmdArgs | Out-Host
|
||||
[int] $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Arguments communs a LPAnimEval / LPAnimTwist.
|
||||
function Get-EvalArgs {
|
||||
$bodies = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.prefixe })) -join ','
|
||||
$templates = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.gabarit })) -join ','
|
||||
'-Methode={0} -ClipDir={1} -RefClip={2} -BodyClipDir={3} -Templates={4} -Bodies={5}' -f (Get-MethodArg), $script:Ch.source.clips, $script:Ch.source.clip_reference, $script:Ch.sortie.clips, $templates, $bodies
|
||||
}
|
||||
function Get-WorkArg([string] $Name) { '-{0}="{1}"' -f $Name, $script:Work }
|
||||
|
||||
function Test-Fresh([string] $Path, [datetime] $Since) {
|
||||
(Test-Path -LiteralPath $Path) -and ((Get-Item -LiteralPath $Path).LastWriteTime -ge $Since.AddSeconds(-2))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- etapes
|
||||
function Step-Inventaire {
|
||||
$packFile = Join-Path $Root "AnimSources\$Pack\pack.json"
|
||||
$script:Cfg = Read-Json $packFile
|
||||
Add-Check 'pack.json lisible' ($null -ne $script:Cfg) (Show-Rel $packFile)
|
||||
if (-not $script:Cfg) { throw "pack.json illisible : $packFile" }
|
||||
$c = $script:Cfg.chaine
|
||||
$script:Ch = $c
|
||||
Add-Check 'bloc chaine present' ($null -ne $c)
|
||||
if (-not $c) { throw 'bloc "chaine" absent de pack.json' }
|
||||
$missing = @()
|
||||
foreach ($k in 'methode', 'profil_attendu', 'mod', 'travail', 'sandbox') { if (Test-Placeholder $c.$k) { $missing += "chaine.$k" } }
|
||||
foreach ($k in 'type', 'clips', 'maillage', 'clip_reference') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } }
|
||||
foreach ($k in 'clips', 'rigs') { if (Test-Placeholder $c.sortie.$k) { $missing += "chaine.sortie.$k" } }
|
||||
if ($c.source.type -eq 'glb') { foreach ($k in 'fichier', 'import') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } } }
|
||||
elseif ($c.source.type -ne 'uasset') { $missing += "chaine.source.type ($($c.source.type) : glb ou uasset)" }
|
||||
if (@($c.corps).Count -eq 0) { $missing += 'chaine.corps' }
|
||||
foreach ($b in @($c.corps)) { foreach ($k in 'nom', 'maillage', 'prefixe', 'gabarit') { if (Test-Placeholder $b.$k) { $missing += "chaine.corps[$($b.nom)].$k" } } }
|
||||
if (Test-Placeholder $script:Cfg.prefixe) { $missing += 'prefixe' }
|
||||
Add-Check 'bloc chaine complet' ($missing.Count -eq 0) ($missing -join ', ')
|
||||
$legal = @()
|
||||
foreach ($k in 'licence', 'credit', 'auteur') { if (Test-Placeholder $script:Cfg.$k) { $legal += "$k = '$($script:Cfg.$k)'" } }
|
||||
Add-Check 'licence, credit et auteur renseignes' ($legal.Count -eq 0) $(if ($legal) { 'refus : ' + ($legal -join ' ; ') } else { "$($script:Cfg.licence) / $($script:Cfg.auteur)" })
|
||||
if ($missing.Count -gt 0) { throw 'bloc chaine incomplet' }
|
||||
$script:Work = (Join-Path $Root ($c.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
|
||||
|
||||
$files = @()
|
||||
if ($c.source.type -eq 'glb') {
|
||||
$glb = [string] $c.source.fichier
|
||||
$ok = Test-Path -LiteralPath $glb
|
||||
Add-Check 'fichier glb trouve' $ok $glb
|
||||
if ($ok) { $script:Etat.glb_sha256 = (Get-FileHash -LiteralPath $glb -Algorithm SHA256).Hash }
|
||||
$imp = Join-Path $Root ($c.source.import -replace '/', '\')
|
||||
Add-Check 'script d''import trouve' (Test-Path $imp) (Show-Rel $imp)
|
||||
}
|
||||
$dir = ConvertTo-Disk $c.source.clips
|
||||
$dirOk = Test-Path $dir
|
||||
if ($c.source.type -eq 'uasset') { Add-Check 'dossier des clips trouve' $dirOk (Show-Rel $dir) }
|
||||
elseif (-not $dirOk) { Write-Host " (dossier des clips absent : cree par l'import) $(Show-Rel $dir)" }
|
||||
$hashes = [ordered]@{}
|
||||
if ($dirOk) {
|
||||
foreach ($clip in Get-SourceClips) {
|
||||
$hashes["$clip.uasset"] = (Get-FileHash -LiteralPath (Join-Path $dir "$clip.uasset") -Algorithm SHA256).Hash
|
||||
}
|
||||
}
|
||||
if ($c.source.type -eq 'uasset') { Add-Check 'empreintes SHA-256 des .uasset' ($hashes.Count -gt 0) "$($hashes.Count) fichier(s)" }
|
||||
else { Write-Host " empreintes SHA-256 : $($hashes.Count) .uasset deja presents + glb" }
|
||||
$script:Etat.empreintes = $hashes
|
||||
$script:St.Extra.empreintes = $hashes
|
||||
if ($script:Etat.glb_sha256) { $script:St.Extra.glb_sha256 = $script:Etat.glb_sha256 }
|
||||
}
|
||||
|
||||
function Step-Import {
|
||||
$c = $script:Ch
|
||||
if ($c.source.type -eq 'uasset') {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host ' source uasset : rien a importer'
|
||||
}
|
||||
else {
|
||||
$dir = ConvertTo-Disk $c.source.clips
|
||||
$have = @(Get-SourceClips).Count
|
||||
$prev = if ($script:PrevEtat) { $script:PrevEtat.glb_sha256 } else { $null }
|
||||
$now = if ($script:Etat.glb_sha256) { $script:Etat.glb_sha256 } elseif (Test-Path -LiteralPath $c.source.fichier) { (Get-FileHash -LiteralPath $c.source.fichier -Algorithm SHA256).Hash } else { $null }
|
||||
$script:Etat.glb_sha256 = $now
|
||||
if ($have -gt 0 -and $prev -and $prev -eq $now) {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host " import saute : $have clip(s) presents, glb inchange (SHA-256 $($now.Substring(0, 12))...)"
|
||||
}
|
||||
else {
|
||||
$py = (Join-Path $Root ($c.source.import -replace '/', '\')) -replace '\\', '/'
|
||||
$log = Join-Path $Root ('Saved\Logs\portage\AP7_Import_{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
Add-Cmd "UnrealEditor-Cmd.exe `"$Root\LifePlay.uproject`" -run=pythonscript `"-script=$py`" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput > $(Show-Rel $log)"
|
||||
if (-not $script:DryRun) {
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
|
||||
& $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript "-script=$py" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log
|
||||
$script:St.Code = $LASTEXITCODE
|
||||
$imported = Select-String -Path $log -Pattern 'QUATERNIUS dossier \d+ asset' -Quiet
|
||||
Add-Check 'import termine (journal)' $imported (Show-Rel $log)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $script:DryRun) {
|
||||
$n = @(Get-SourceClips).Count
|
||||
Add-Check 'clips source > 0' ($n -gt 0) "$n clip(s)"
|
||||
$script:Clips = Get-SourceClips
|
||||
}
|
||||
else { Write-Host " clips source actuellement : $(@(Get-SourceClips).Count)" }
|
||||
}
|
||||
|
||||
function Step-Identification {
|
||||
$c = $script:Ch
|
||||
$clip = '{0}/{1}' -f $c.source.clips.TrimEnd('/'), $c.source.clip_reference
|
||||
$out = "$($script:Work)/identification.json"
|
||||
$code = Invoke-Commandlet 'LPAnimIdentify' ('-Clip={0} -Out="{1}" -Attendu={2}' -f $clip, $out, $c.profil_attendu)
|
||||
if ($script:DryRun) {
|
||||
if ($c.methode -and $c.methode -ne 'auto') { Write-Host " methode imposee par pack.json : $($c.methode)" } else { Write-Host ' methode retenue : celle de identification.json (champ methode)' }
|
||||
return
|
||||
}
|
||||
$script:St.Code = $code
|
||||
Add-Check 'LPAnimIdentify code 0' ($code -eq 0) "code $code"
|
||||
$r = Read-Json $out
|
||||
Add-Check 'identification.json valide' ($r -and $r.valide -eq $true) $(if ($r) { "profil $($r.profil), methode $($r.methode)" } else { 'rapport absent' })
|
||||
Add-Check 'methode non factice' ($r -and $r.methode -and $r.methode_factice -ne $true) $(if ($r) { "methode '$($r.methode)'" })
|
||||
if ($c.methode -and $c.methode -ne 'auto') { $script:Method = [string] $c.methode; Write-Host " methode imposee par pack.json : $($script:Method)" }
|
||||
elseif ($r -and $r.methode) { $script:Method = [string] $r.methode }
|
||||
$script:Etat.methode = $script:Method
|
||||
$script:St.Extra.methode = $script:Method
|
||||
}
|
||||
|
||||
function Step-Calque {
|
||||
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$a = (Get-EvalArgs) + " -PoseOnly -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimEval' $a
|
||||
if ($script:DryRun) { return }
|
||||
$script:St.Code = $code
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport.json')
|
||||
Add-Check 'rapport.json valide (pose T)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' } else { "code $code" })
|
||||
foreach ($b in @($script:Ch.corps)) {
|
||||
Add-Check "retarget_pose_$($b.nom).txt ecrit" (Test-Path (Join-Path $script:Work "pose\retarget_pose_$($b.nom).txt"))
|
||||
}
|
||||
}
|
||||
|
||||
function Step-Conversion {
|
||||
$c = $script:Ch
|
||||
$cfgFile = "$($script:Work)/anim_retarget.json"
|
||||
$cfg = [ordered]@{
|
||||
mode = 'bake'; methode = (Get-MethodArg); source_mesh = $c.source.maillage; clips_dir = $c.source.clips
|
||||
out_dir = $c.sortie.clips; dest = $c.sortie.rigs
|
||||
corps = @($c.corps | ForEach-Object { [ordered]@{ nom = $_.nom; maillage = $_.maillage; prefixe = $_.prefixe } })
|
||||
exclure = @($script:Cfg.exclus | Where-Object { $_ })
|
||||
pose_dir = "$($script:Work)/pose"
|
||||
}
|
||||
$log = "$($script:Work)/etapes/4_conversion_bake.log"
|
||||
$bat = "$($script:Work)/etapes/4_conversion_bake.cmd"
|
||||
$buildMod = Join-Path $Root 'Tools\BuildMod.ps1'
|
||||
$line = "@`"$Pwsh`" -NoProfile -ExecutionPolicy Bypass -File `"$buildMod`" -Retarget bake -RetargetOnly -RetargetConfig `"$($cfgFile -replace '/', '\')`" > `"$($log -replace '/', '\')`" 2>&1"
|
||||
Write-Host " config de cuisson : $(Show-Rel $cfgFile)"
|
||||
Add-Cmd "cmd /c start `"`" /wait `"$(Show-Rel $bat)`" (contenu : $(Show-Rel $buildMod) -Retarget bake -RetargetOnly -RetargetConfig $(Show-Rel $cfgFile) > $(Show-Rel $log))"
|
||||
if (-not $script:DryRun) {
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
Write-Text $cfgFile (($cfg | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
Write-Text $bat ($line + "`r`n@exit /b %ERRORLEVEL%`r`n")
|
||||
# start /wait sur un .cmd laisse sa console ouverte (cmd /K) et ne rend jamais la main : passer par cmd /c (AP7).
|
||||
$p = Start-Process cmd -ArgumentList ('/c start "" /wait cmd /c "{0}"' -f ($bat -replace '/', '\')) -Wait -PassThru -NoNewWindow
|
||||
$script:St.Code = $p.ExitCode
|
||||
$text = if (Test-Path $log) { Get-Content $log -Raw -Encoding utf8 } else { '' }
|
||||
$n = @(Get-SourceClips).Count
|
||||
$m = [regex]::Match($text, 'RETARGET bridget (\d+) michael (\d+)')
|
||||
Add-Check 'conversion : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
|
||||
Add-Check "ligne RETARGET bridget $n michael $n" ($m.Success -and [int] $m.Groups[1].Value -eq $n -and [int] $m.Groups[2].Value -eq $n) $(if ($m.Success) { $m.Value } else { 'ligne absente : voir ' + (Show-Rel $log) })
|
||||
if (-not $m.Success) { throw 'conversion non confirmee' }
|
||||
}
|
||||
$a = (Get-EvalArgs) + ' ' + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimTwist' $a
|
||||
if ($script:DryRun) { return }
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport_torsion.json')
|
||||
Add-Check 'rapport_torsion.json valide' ($r -and $r.valide -eq $true) $(if (-not $r) { "rapport absent (code $code)" } else { '' })
|
||||
}
|
||||
|
||||
# Sandbox : vignettes et planches. Fichiers de Saved\ remplaces le temps de la tournee, puis deplaces ou restaures.
|
||||
function Invoke-Sandbox {
|
||||
$c = $script:Ch
|
||||
$saved = Join-Path $Root 'Saved'
|
||||
$ini = Join-Path $saved 'sandbox_pack.ini'
|
||||
$modeF = Join-Path $saved 'sandbox_mode.txt'
|
||||
$tourF = Join-Path $saved 'sandbox_tour.txt'
|
||||
$bat = Join-Path $Root 'Tools\debug\LifePlay_interaction_sandbox.bat'
|
||||
$sbRel = ($c.sortie.clips -replace '^/Game/', '')
|
||||
$clipRel = ($c.source.clips -replace '^/Game/', '')
|
||||
$sbRoot = [string] $c.sandbox
|
||||
$iniText = @(
|
||||
'; Ecrit par Tools\anim\AnimPipeline.ps1 (lot AP7)', '[Sandbox.Pack]', "Name=$Pack", "Method=$(Get-MethodArg)",
|
||||
"ClipRoot=$($c.source.clips)", "ClipDisk=$clipRel", "BodyClipRoot=$($c.sortie.clips)", "BodyClipDisk=$sbRel",
|
||||
"RefClip=$($c.source.clip_reference)", "MannequinMesh=$($c.source.maillage)", '', '[Sandbox.Output]', "MeasureOutputRoot=$sbRoot") -join "`r`n"
|
||||
$outDir = Join-Path $saved ($sbRoot -replace '/', '\')
|
||||
Add-Cmd "ecrire Saved\sandbox_pack.ini (Name=$Pack, Method=$(Get-MethodArg), ClipRoot=$($c.source.clips), BodyClipRoot=$($c.sortie.clips), RefClip=$($c.source.clip_reference), MeasureOutputRoot=$sbRoot)"
|
||||
Add-Cmd 'ecrire Saved\sandbox_mode.txt = Mesure ; Saved\sandbox_tour.txt = vignettes planches quitter'
|
||||
Add-Cmd "Tools\debug\LifePlay_interaction_sandbox.bat (attente de la fermeture de UnrealEditor, 40 min maximum)"
|
||||
Add-Cmd "deplacer Saved\sandbox_pack.ini vers $(Get-WorkRel)\sandbox_pack.ini ; restaurer sandbox_mode.txt d'origine"
|
||||
if ($script:DryRun) { Write-Host " controle : <Clip>.png dans $(Show-Rel (Join-Path $outDir 'vignettes')) ; <Clip>.jpg et <Clip>.json dans $(Show-Rel (Join-Path $outDir 'planches')), par clip"; return }
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$t0 = Get-Date
|
||||
New-Item -ItemType Directory -Force $script:Work | Out-Null
|
||||
$origMode = if (Test-Path $modeF) { Get-Content $modeF -Raw } else { $null }
|
||||
foreach ($f in $ini, $tourF) {
|
||||
if (Test-Path $f) { Move-Item $f (Join-Path $script:Work ("{0}.avant_{1}" -f (Split-Path $f -Leaf), $stamp)) }
|
||||
}
|
||||
try {
|
||||
Write-Text $ini ($iniText + "`r`n")
|
||||
Write-Text $modeF 'Mesure'
|
||||
Write-Text $tourF 'vignettes planches quitter'
|
||||
# Sans tuyau ni attente : avec `& $bat | Out-Null`, PowerShell attendait la fin de l'editeur lance par le .bat
|
||||
# (heritier de la sortie), puis ne le trouvait plus et concluait a tort « pas lancee » (AP7).
|
||||
Start-Process -FilePath 'cmd.exe' -ArgumentList ('/c "{0}"' -f $bat) -WindowStyle Hidden | Out-Null
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
while (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 3 }
|
||||
if (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue)) { throw 'la sandbox ne s''est pas lancee (3 min)' }
|
||||
$deadline = (Get-Date).AddMinutes(40)
|
||||
while ((Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 10 }
|
||||
if (Get-Process UnrealEditor -ErrorAction SilentlyContinue) { throw 'la sandbox ne s''est pas fermee en 40 min (laissee ouverte)' }
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $ini) { Move-Item $ini (Join-Path $script:Work 'sandbox_pack.ini') -Force }
|
||||
if (Test-Path $tourF) { Move-Item $tourF (Join-Path $script:Work 'sandbox_tour.txt') -Force }
|
||||
if (Test-Path $modeF) { Move-Item $modeF (Join-Path $script:Work 'sandbox_mode.txt') -Force }
|
||||
if ($null -ne $origMode) { Write-Text $modeF $origMode }
|
||||
}
|
||||
$missV = @(); $missP = @()
|
||||
foreach ($clip in (Get-SourceClips)) {
|
||||
if (-not (Test-Fresh (Join-Path $outDir "vignettes\$clip.png") $t0)) { $missV += $clip }
|
||||
if (-not ((Test-Fresh (Join-Path $outDir "planches\$clip.jpg") $t0) -and (Test-Fresh (Join-Path $outDir "planches\$clip.json") $t0))) { $missP += $clip }
|
||||
}
|
||||
Add-Check 'une vignette <Clip>.png par clip' ($missV.Count -eq 0) $(if ($missV) { 'manquantes : ' + (($missV | Select-Object -First 8) -join ', ') })
|
||||
Add-Check 'une planche <Clip>.jpg + .json par clip' ($missP.Count -eq 0) $(if ($missP) { 'manquantes : ' + (($missP | Select-Object -First 8) -join ', ') })
|
||||
}
|
||||
|
||||
function Step-Evaluation {
|
||||
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$a = (Get-EvalArgs) + " -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimEval' $a
|
||||
Add-Cmd "python Tools\anim\RapportControles.py $(Get-WorkRel)"
|
||||
if (-not $script:DryRun) {
|
||||
$script:St.Code = $code
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport.json')
|
||||
Add-Check 'rapport.json valide (aucun clip rejete)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' })
|
||||
if ($r) { foreach ($x in @($r.a_revoir)) { Add-Warn "a revoir : $x" } }
|
||||
if (-not ($r -and $r.valide -eq $true)) { throw 'evaluation en echec' }
|
||||
& python (Join-Path $Root 'Tools\anim\RapportControles.py') $script:Work | Out-Host
|
||||
Add-Check 'rapport.html ecrit' ((Test-Path (Join-Path $script:Work 'rapport.html')) -and $LASTEXITCODE -eq 0)
|
||||
}
|
||||
Invoke-Sandbox
|
||||
}
|
||||
|
||||
function Get-DeliverParams([switch] $CatalogueOnly) {
|
||||
$c = $script:Ch
|
||||
$outDir = Join-Path (Join-Path $Root 'Saved') (($c.sandbox -replace '/', '\'))
|
||||
$p = [ordered]@{
|
||||
Name = $c.mod; Source = $c.sortie.clips; Prefix = $script:Cfg.prefixe; Exclude = (@($script:Cfg.exclus | Where-Object { $_ }) -join ',')
|
||||
Pipeline = (Get-WorkRel); Pack = (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
}
|
||||
if ($CatalogueOnly) {
|
||||
$p.Vignettes = Join-Path $outDir 'vignettes'; $p.Planches = Join-Path $outDir 'planches'; $p.CatalogueOnly = $true
|
||||
}
|
||||
$p
|
||||
}
|
||||
function Format-Params($P) { (@($P.GetEnumerator() | ForEach-Object { if ($_.Value -is [bool]) { "-$($_.Key)" } else { "-$($_.Key) '$($_.Value)'" } })) -join ' ' }
|
||||
function Invoke-Deliver($P) {
|
||||
Add-Cmd "Tools\anim\DeliverAnims.ps1 $(Format-Params $P)"
|
||||
if ($script:DryRun) { return 0 }
|
||||
$global:LASTEXITCODE = 0
|
||||
& (Join-Path $Root 'Tools\anim\DeliverAnims.ps1') @P | Out-Host
|
||||
[int] $LASTEXITCODE
|
||||
}
|
||||
|
||||
function Step-Catalogue {
|
||||
$c = $script:Ch
|
||||
$liv = Join-Path $script:Work 'livraison.json'
|
||||
$fic = Join-Path $script:Work 'fiches_calculees.json'
|
||||
$t0 = Get-Date
|
||||
# a. copie des clips dans le mod (ecrit livraison.json)
|
||||
$code = Invoke-Deliver (Get-DeliverParams)
|
||||
if (-not $script:DryRun) {
|
||||
$script:St.Code = $code
|
||||
Add-Check 'livraison (copie dans le mod) : code 0' ($code -eq 0) "code $code"
|
||||
Add-Check 'livraison.json ecrit' (Test-Fresh $liv $t0)
|
||||
if ($code -ne 0 -or -not (Test-Fresh $liv $t0)) { throw 'livraison en echec' }
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
}
|
||||
# b. fiches calculees
|
||||
$code = Invoke-Commandlet 'LPAnimCatalog' ('-Livraison="{0}" -Out="{1}"' -f "$($script:Work)/livraison.json", $script:Work)
|
||||
if (-not $script:DryRun) {
|
||||
$f = Read-Json $fic
|
||||
$newer = (Test-Path $fic) -and (Test-Path $liv) -and ((Get-Item $fic).LastWriteTime -gt (Get-Item $liv).LastWriteTime)
|
||||
Add-Check 'fiches_calculees.json plus recent que livraison.json' $newer
|
||||
Add-Check 'fiches calculees sans echec' ($f -and @($f.echecs).Count -eq 0 -and $code -eq 0) $(if ($f -and $f.echecs) { (@($f.echecs) -join '; ') } else { "code $code" })
|
||||
if (-not $newer) { throw 'fiches non calculees' }
|
||||
}
|
||||
# c. catalogue (Catalogue.py)
|
||||
$code = Invoke-Deliver (Get-DeliverParams -CatalogueOnly)
|
||||
if (-not $script:DryRun) {
|
||||
$r = Read-Json (Join-Path $script:Work 'catalogue_rapport.json')
|
||||
$cat = Join-Path $Root "ModSources\$($c.mod)\Module\Animations\catalogue.json"
|
||||
Add-Check 'Catalogue.py : code 0' ($code -eq 0) "code $code"
|
||||
Add-Check 'catalogue_rapport.json ok' ($r -and $r.ok -eq $true) $(if ($r) { "$($r.fiches) fiche(s)" })
|
||||
Add-Check 'catalogue.json dans le mod' (Test-Path $cat) (Show-Rel $cat)
|
||||
}
|
||||
}
|
||||
|
||||
function Step-Livraison {
|
||||
$c = $script:Ch
|
||||
$version = (Select-String -Path (Join-Path $Root 'Config\DefaultGame.ini') -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1).Matches[0].Groups[1].Value.Trim()
|
||||
$release = Join-Path $Root "Releases\$version\Windows\AssetRegistry.bin"
|
||||
$log = "$($script:Work)/etapes/7_livraison_cuisson.log"
|
||||
if ($SansCuisson) {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host ' -SansCuisson : cuisson DLC sautee'
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path $release)) {
|
||||
Add-Cmd "(saute) Tools\BuildMod.ps1 -Name $($c.mod) version de reference absente : $(Show-Rel $release)"
|
||||
Add-Warn 'version de reference absente : cuisson DLC non faite'
|
||||
return
|
||||
}
|
||||
Add-Cmd "$(Split-Path $Pwsh -Leaf) -NoProfile -File Tools\BuildMod.ps1 -Name $($c.mod) > $(Show-Rel $log) (version de reference $version)"
|
||||
if ($script:DryRun) { return }
|
||||
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
|
||||
& $Pwsh -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'Tools\BuildMod.ps1') -Name $c.mod *>&1 | Tee-Object -FilePath $log | Out-Host
|
||||
$script:St.Code = $LASTEXITCODE
|
||||
$text = Get-Content $log -Raw -Encoding utf8
|
||||
$m = [regex]::Match($text, 'Archive : (.+?\.zip) \(')
|
||||
Add-Check 'BuildMod : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
|
||||
Add-Check 'archive .zip produite' ($m.Success -and (Test-Path $m.Groups[1].Value)) $(if ($m.Success) { $m.Groups[1].Value })
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- moteur
|
||||
function Save-Etat {
|
||||
if ($script:DryRun) { return }
|
||||
$script:Etat.maj = Get-Date -Format s
|
||||
Write-Text (Join-Path $script:Work 'etat.json') (($script:Etat | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
}
|
||||
function Move-Previous {
|
||||
$w = $script:Work
|
||||
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
|
||||
if (-not $keep.Count) { return }
|
||||
$dest = Join-Path $w ("precedent_{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
New-Item -ItemType Directory -Force $dest | Out-Null
|
||||
foreach ($k in $keep) { Move-Item -LiteralPath $k.FullName -Destination $dest }
|
||||
Write-Host " resultat precedent deplace : $($keep.Count) element(s) -> $(Show-Rel $dest)"
|
||||
}
|
||||
function Invoke-Step([int] $N, [scriptblock] $Block) {
|
||||
if ($N -lt $From -or $N -gt $To) { return }
|
||||
$name = $StepNames[$N]
|
||||
Write-Host ''
|
||||
Write-Host "== Etape $N : $name"
|
||||
$script:St = [ordered]@{ N = $N; Cmds = @(); Checks = @(); Warns = @(); Extra = [ordered]@{}; Code = $null; Skipped = $false }
|
||||
$t0 = Get-Date
|
||||
try { $null = & $Block }
|
||||
catch {
|
||||
$msg = $_.Exception.Message
|
||||
$script:St.Checks += [ordered]@{ nom = 'erreur'; ok = $false; detail = $msg }
|
||||
if ($script:DryRun) { $script:Planned += "etape $N : $msg"; Write-Host " [ECHEC PREVU] $msg" -ForegroundColor Yellow }
|
||||
else { Write-Host " [ECHEC] $msg" -ForegroundColor Red }
|
||||
}
|
||||
$bad = @($script:St.Checks | Where-Object { $_.ok -eq $false })
|
||||
$verdict = if ($bad.Count) { 'echec' } elseif ($script:St.Skipped) { 'saute' } elseif ($script:St.Warns.Count) { 'avertissement' } else { 'ok' }
|
||||
if ($script:DryRun) { return $(if ($N -eq 0 -and -not $script:Ch) { 'stop' }) }
|
||||
if (-not $script:Work) { throw "Etape $N ($name) en echec : $((@($bad | ForEach-Object { $_.detail })) -join ' ; ')" }
|
||||
# Passage complet : l'ancien resultat est deplace une fois l'inventaire accepte (un refus ne touche a rien).
|
||||
$recName = "${N}_$name"
|
||||
if ($N -eq 0 -and $From -eq 0) {
|
||||
if ($bad.Count) { $recName = "${N}_${name}_refus_$(Get-Date -Format 'yyyyMMdd-HHmmss')" } else { Move-Previous }
|
||||
}
|
||||
$t1 = Get-Date
|
||||
$rec = [ordered]@{
|
||||
etape = $N; nom = $name; debut = $t0.ToString('s'); fin = $t1.ToString('s'); duree_s = [int] ($t1 - $t0).TotalSeconds
|
||||
commandes = $script:St.Cmds; code = $script:St.Code; controles = $script:St.Checks; avertissements = $script:St.Warns; verdict = $verdict
|
||||
}
|
||||
foreach ($k in $script:St.Extra.Keys) { $rec[$k] = $script:St.Extra[$k] }
|
||||
Write-Text (Join-Path $script:Work "etapes\$recName.json") (($rec | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
$script:Etat.etapes["$N"] = [ordered]@{ nom = $name; verdict = $verdict; fin = $t1.ToString('s') }
|
||||
Save-Etat
|
||||
Write-Host " verdict : $verdict ($($rec.duree_s) s)"
|
||||
if ($verdict -eq 'echec') {
|
||||
throw "Etape $N ($name) en echec : " + ((@($bad | ForEach-Object { "$($_.nom)$(if ($_.detail) { " ($($_.detail))" })" })) -join ' ; ')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Pack -notmatch '^[A-Za-z0-9_-]+$') { throw "nom de pack invalide : $Pack" }
|
||||
if ($From -gt $To) { throw "-From ($From) apres -To ($To)" }
|
||||
Write-Host "Pilote AnimPipeline : pack $Pack, etapes $From a $To$(if ($DryRun) { ' (A BLANC : rien lance, rien cree)' })"
|
||||
|
||||
# Le bloc chaine est lu des l'etape 0 ; pour -From > 0 il faut le charger sans rejouer l'inventaire.
|
||||
if ($From -gt 0) {
|
||||
$script:Cfg = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
if (-not $script:Cfg -or -not $script:Cfg.chaine) { throw "pack.json illisible ou sans bloc chaine (AnimSources\$Pack)" }
|
||||
$script:Ch = $script:Cfg.chaine
|
||||
$script:Work = (Join-Path $Root ($script:Ch.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
|
||||
$prev = if (Test-Path (Join-Path $script:Work 'etat.json')) { Get-Content (Join-Path $script:Work 'etat.json') -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable } else { $null }
|
||||
if ($prev) { $script:Etat = $prev; $script:PrevEtat = $prev; if ($prev.methode) { $script:Method = [string] $prev.methode } }
|
||||
if (-not $script:Method -and $script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Method = [string] $script:Ch.methode }
|
||||
for ($i = 0; $i -lt $From; $i++) {
|
||||
$e = if ($prev -and $prev.etapes) { $prev.etapes["$i"] } else { $null }
|
||||
if (-not $e -or $e.verdict -notin 'ok', 'saute', 'avertissement') {
|
||||
if ($DryRun) { Write-Host " (etape $i : pas de verdict enregistre)" } else { Write-Warning "etape $i : pas de verdict enregistre dans etat.json (reprise sur la foi de l'utilisateur)" }
|
||||
}
|
||||
}
|
||||
if ($From -ge 3 -and -not $script:Method -and -not $DryRun) { throw "methode inconnue (etat.json sans methode et pack.json = auto) : reprendre depuis l'etape 2" }
|
||||
$script:Clips = Get-SourceClips
|
||||
}
|
||||
else {
|
||||
# Passage complet : etat precedent lu avant tout deplacement.
|
||||
$cfgPeek = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
if ($cfgPeek -and $cfgPeek.chaine.travail) {
|
||||
$w = (Join-Path $Root ($cfgPeek.chaine.travail -replace '/', '\')).TrimEnd('\')
|
||||
$ef = Join-Path $w 'etat.json'
|
||||
if (Test-Path $ef) { $script:PrevEtat = Get-Content $ef -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable }
|
||||
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
|
||||
if ($keep.Count) { Write-Host "Resultat precedent : $($keep.Count) element(s) de $(Show-Rel $w) a deplacer dans $(Show-Rel $w)\precedent_<date> apres l'inventaire accepte" }
|
||||
}
|
||||
}
|
||||
|
||||
$stop = $null
|
||||
$stop = Invoke-Step 0 { Step-Inventaire }
|
||||
if ($stop -eq 'stop') { throw 'plan interrompu : pack.json illisible ou sans bloc chaine' }
|
||||
Invoke-Step 1 { Step-Import }
|
||||
Invoke-Step 2 { Step-Identification }
|
||||
Invoke-Step 3 { Step-Calque }
|
||||
Invoke-Step 4 { Step-Conversion }
|
||||
Invoke-Step 5 { Step-Evaluation }
|
||||
Invoke-Step 6 { Step-Catalogue }
|
||||
Invoke-Step 7 { Step-Livraison }
|
||||
|
||||
Write-Host ''
|
||||
if ($DryRun) {
|
||||
if ($script:Planned.Count) {
|
||||
Write-Host "A blanc : plan complet affiche ; $($script:Planned.Count) echec(s) prevu(s) :" -ForegroundColor Yellow
|
||||
$script:Planned | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
|
||||
}
|
||||
else { Write-Host 'A blanc : plan complet affiche, aucun echec prevu' }
|
||||
}
|
||||
else { Write-Host "Chaine terminee : pack $Pack, etapes $From a $To (rapports : $(Get-WorkRel)\etapes)" }
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Catalogue des animations d'un mod (lot AP6) : catalogue.json, catalogue.html, vignettes.
|
||||
|
||||
Entrees : pack.json (declare), livraison.json + fiches_calculees.json + rapport.json + csv de qualite (calcule),
|
||||
vignettes Saved/Measurement/vignettes/<Clip>.png (carres 512 px), planches Saved/Measurement/planches/<Clip>.jpg + .json
|
||||
(bande de N images carrees de 256 px, animee au survol), clips du mod ModSources/<mod>/Content/Anims.
|
||||
Entrees : pack.json (--pack), livraison.json + fiches_calculees.json + rapport.json + csv de qualite (dossier --pipeline),
|
||||
vignettes <Clip>.png (--vignettes, carres 512 px, defaut Saved/Measurement/vignettes), planches <Clip>.jpg + .json
|
||||
(--planches, defaut Saved/Measurement/planches ; bande de N images carrees de 256 px, animee au survol),
|
||||
clips du mod ModSources/<mod>/Content/Anims (ou --mod-dir). Collisions de cles : jeu de base, scenarios du jeu et
|
||||
catalogues de tous les autres mods de ModSources/*/Module/Animations/catalogue.json.
|
||||
Controles bloquants : code de sortie 1 et liste des causes ; rien n'est ecrit dans --out en cas d'echec.
|
||||
Python 3 standard, sans dependance.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
Le jeu appelle Actor.animate(<cle>) et cherche <cle>_F et <cle>_M (cle = ce qui precede le dernier _).
|
||||
1. controle bloquant : rapport.json existe et valide = vrai (les a_revoir sont affiches, non bloquants) ;
|
||||
2. controles : editeur ferme, aucun autre plugin dans Mods\ ;
|
||||
3. table cle / sources / destinations calculee depuis Content\Quaternius\UAL\B_*.uasset et M_*.uasset ;
|
||||
3. table cle / sources / destinations calculee depuis le dossier disque de -Source (/Game/X/Y -> Content\X\Y), B_*.uasset et M_*.uasset ;
|
||||
4. -DryRun : affiche la table et s'arrete (rien n'est lance ni cree) ;
|
||||
5. sinon : cree ModSources\<Nom> s'il manque (jamais d'ecrasement), monte le mod, lance Tools\anim\DeliverAnims.py
|
||||
dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_<horodatage>.log), remet le mod en place,
|
||||
|
|
@ -16,8 +16,13 @@
|
|||
-Catalogue : apres une livraison reussie, lance Tools\anim\Catalogue.py (lot AP6).
|
||||
-CatalogueOnly : saute la copie et ne produit que le catalogue (rapport valide exige), sans lancer Unreal.
|
||||
|
||||
Parametres (defaut = pack Quaternius) : -Pipeline (dossier des rapports, defaut de -Report, lieu de livraison.json ; sous Saved\),
|
||||
-Pack (pack.json ; donne aussi, quand il est passe explicitement, les textes du mod cree : source, auteur, licence, credit, url ;
|
||||
refus si licence, credit ou auteur est vide ou "A COMPLETER" ; sans -Pack, textes Quaternius d'origine), -Vignettes / -Planches (dossiers transmis a Catalogue.py), -Source (clips convertis B_/M_), -Name, -Prefix, -Exclude.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\anim\DeliverAnims.ps1 -DryRun
|
||||
.\Tools\anim\DeliverAnims.ps1 -DryRun -Pipeline Saved\AnimPipeline\freeanims -Source /Game/AnimPacks/FreeAnimationsPack -Name lpr_anim_freeanims -Prefix fap -Exclude ''
|
||||
.\Tools\anim\DeliverAnims.ps1
|
||||
#>
|
||||
param(
|
||||
|
|
@ -26,6 +31,10 @@ param(
|
|||
[string] $Prefix = 'qua',
|
||||
[string] $Exclude = 'A_TPose',
|
||||
[string] $Report = '',
|
||||
[string] $Pipeline = 'Saved\AnimPipeline\quaternius',
|
||||
[string] $Pack = 'AnimSources\quaternius\pack.json',
|
||||
[string] $Vignettes = '',
|
||||
[string] $Planches = '',
|
||||
[switch] $DryRun,
|
||||
[switch] $Catalogue,
|
||||
[switch] $CatalogueOnly
|
||||
|
|
@ -35,7 +44,13 @@ $ErrorActionPreference = 'Stop'
|
|||
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $Root 'Tools\ModSource.ps1')
|
||||
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
if (-not $Report) { $Report = Join-Path $Root 'Saved\AnimPipeline\quaternius\rapport.json' }
|
||||
# -Pipeline : dossier des rapports du pack (relatif a la racine du projet, ou absolu) ; defaut de -Report, lieu de livraison.json.
|
||||
# -Report seul (sans -Pipeline) : le dossier du rapport fait office de dossier de pipeline (comportement d'avant).
|
||||
if ($Report -and -not $PSBoundParameters.ContainsKey('Pipeline')) { $Pipeline = Split-Path $Report -Parent }
|
||||
if (-not [IO.Path]::IsPathRooted($Pipeline)) { $Pipeline = Join-Path $Root $Pipeline }
|
||||
$Pipeline = $Pipeline.TrimEnd('\', '/')
|
||||
if (-not $Report) { $Report = Join-Path $Pipeline 'rapport.json' }
|
||||
if (-not [IO.Path]::IsPathRooted($Pack)) { $Pack = Join-Path $Root $Pack }
|
||||
|
||||
function New-IfMissing([string] $Path, [string] $Text) {
|
||||
if (Test-Path $Path) { return }
|
||||
|
|
@ -51,11 +66,13 @@ function Invoke-Catalogue {
|
|||
$line = Select-String -Path $lpmodPath -Pattern '^MODULE_VERSION:\s*(\S+)' | Select-Object -First 1
|
||||
if (-not $line) { throw "MODULE_VERSION absente de $lpmodPath" }
|
||||
$version = $line.Matches[0].Groups[1].Value
|
||||
$pipeline = Split-Path $Report -Parent
|
||||
$out = Join-Path $Root "ModSources\$Name\Module\Animations"
|
||||
Write-Host "Catalogue : version $version -> $out"
|
||||
& python (Join-Path $Root 'Tools\anim\Catalogue.py') --pack (Join-Path $Root 'AnimSources\quaternius\pack.json') `
|
||||
--pipeline $pipeline --mod $Name --version $version --out $out
|
||||
$extra = @()
|
||||
if ($Vignettes) { $extra += @('--vignettes', $Vignettes) }
|
||||
if ($Planches) { $extra += @('--planches', $Planches) }
|
||||
& python (Join-Path $Root 'Tools\anim\Catalogue.py') --pack $Pack `
|
||||
--pipeline $Pipeline --mod $Name --version $version --out $out @extra
|
||||
if ($LASTEXITCODE -ne 0) { throw "Catalogue.py a echoue (code $LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
|
|
@ -115,26 +132,44 @@ try {
|
|||
exit 0
|
||||
}
|
||||
|
||||
# 5. Sources du mod (jamais d'ecrasement).
|
||||
# 5. Sources du mod (jamais d'ecrasement). Textes : pack.json si -Pack est donne, sinon ceux de Quaternius.
|
||||
$paths = Get-ModSourcePaths $Root $Name
|
||||
$src = $paths.Source
|
||||
$title = 'Quaternius Animations'
|
||||
$desc = 'Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay'
|
||||
$author = 'LifePlay Revival (animations by Quaternius)'
|
||||
$uplugin = [ordered]@{
|
||||
FileVersion = 3; Version = 1; VersionName = '1.0'; FriendlyName = 'Quaternius Animations'
|
||||
Description = $desc; Category = 'LifePlay Mods'; CreatedBy = $author
|
||||
CanContainContent = $true; IsBetaVersion = $false; Installed = $false
|
||||
CreatedByURL = 'https://quaternius.com'
|
||||
}
|
||||
New-IfMissing (Join-Path $src "$Name.uplugin") (($uplugin | ConvertTo-Json) + "`r`n")
|
||||
$lpmod = @(
|
||||
"MODULE_UNIQUEID: $Name", 'MODULE_NAME: Quaternius Animations', "MODULE_AUTHOR: $author",
|
||||
'MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival', "MODULE_DESCRIPTION: $desc",
|
||||
'MODULE_REQUIREMENTS: vin_Base', 'MODULE_VERSION: 1.0', "MODULE_CREATED: $(Get-Date -Format 'yyyy-MM-dd')",
|
||||
'MODULE_LICENSE: CC0', 'MODULE_AUTHOR_LINK: https://quaternius.com', 'MODULE_PRICE: Free', 'MODULE_DEFAULT_ENABLED: true')
|
||||
New-IfMissing (Join-Path $src "Module\$Name.lpmod") (($lpmod -join "`r`n") + "`r`n")
|
||||
$licence = 'CC0'
|
||||
$link = 'https://quaternius.com'
|
||||
$credits = "Credits and licenses of the assets in Quaternius Animations (one line per asset: title, author, license, source URL, changes).`r`n" +
|
||||
"- Universal Animation Library by Quaternius, CC0 1.0, https://quaternius.com . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed qua_<clip>_F / qua_<clip>_M.`r`n"
|
||||
if ($PSBoundParameters.ContainsKey('Pack')) {
|
||||
$pk = Get-Content $Pack -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
foreach ($k in 'source', 'auteur', 'licence', 'credit') {
|
||||
if (-not ([string] $pk.$k).Trim() -or ([string] $pk.$k) -match 'A COMPLETER') { throw "pack.json : '$k' vide ou 'A COMPLETER' (mod non cree)" }
|
||||
}
|
||||
$title = [string] $pk.source
|
||||
$desc = "Free animation clips from $($pk.source) ($($pk.licence)), retargeted for LifePlay"
|
||||
$author = "LifePlay Revival (animations by $($pk.auteur))"
|
||||
$licence = [string] $pk.licence
|
||||
# Lien de l'auteur : url_auteur (page de l'auteur) s'il est donne, sinon url (page du pack).
|
||||
$authorUrl = if ($pk.PSObject.Properties['url_auteur']) { [string] $pk.url_auteur } else { [string] $pk.url }
|
||||
$link = if ($authorUrl.Trim() -and $authorUrl -notmatch 'A COMPLETER') { $authorUrl } else { '' }
|
||||
$credits = "Credits and licenses of the assets in $title (one line per asset: title, author, license, source URL, changes).`r`n" +
|
||||
"- $($pk.credit)$(if ($pk.url -and ([string] $pk.url) -notmatch 'A COMPLETER') { ', ' + $pk.url }) . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed $($pk.prefixe)_<clip>_F / $($pk.prefixe)_<clip>_M.`r`n"
|
||||
}
|
||||
$uplugin = [ordered]@{
|
||||
FileVersion = 3; Version = 1; VersionName = '1.0'; FriendlyName = $title
|
||||
Description = $desc; Category = 'LifePlay Mods'; CreatedBy = $author
|
||||
CanContainContent = $true; IsBetaVersion = $false; Installed = $false
|
||||
}
|
||||
if ($link) { $uplugin.CreatedByURL = $link }
|
||||
New-IfMissing (Join-Path $src "$Name.uplugin") (($uplugin | ConvertTo-Json) + "`r`n")
|
||||
$lpmod = @(
|
||||
"MODULE_UNIQUEID: $Name", "MODULE_NAME: $title", "MODULE_AUTHOR: $author",
|
||||
'MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival', "MODULE_DESCRIPTION: $desc",
|
||||
'MODULE_REQUIREMENTS: vin_Base', 'MODULE_VERSION: 1.0', "MODULE_CREATED: $(Get-Date -Format 'yyyy-MM-dd')",
|
||||
"MODULE_LICENSE: $licence") + @($(if ($link) { "MODULE_AUTHOR_LINK: $link" })) + @('MODULE_PRICE: Free', 'MODULE_DEFAULT_ENABLED: true')
|
||||
New-IfMissing (Join-Path $src "Module\$Name.lpmod") (($lpmod -join "`r`n") + "`r`n")
|
||||
New-IfMissing (Join-Path $src 'Module\CREDITS.txt') $credits
|
||||
New-Item -ItemType Directory -Force (Join-Path $src 'Content') | Out-Null
|
||||
|
||||
|
|
@ -145,7 +180,12 @@ try {
|
|||
$py = (Join-Path $Root 'Tools\anim\DeliverAnims.py') -replace '\\', '/'
|
||||
$mounted = Mount-ModSource $Root $Name
|
||||
try {
|
||||
$script = "-script=$py mod=$Name src=$Source prefix=$Prefix exclude=$Exclude"
|
||||
# Le script Python decoupe ses arguments sur les espaces : pipeline= est donne relatif a Saved\ (sans espace).
|
||||
$savedDir = (Join-Path $Root 'Saved').TrimEnd('\') + '\'
|
||||
if (-not $Pipeline.StartsWith($savedDir, [StringComparison]::OrdinalIgnoreCase)) { throw "-Pipeline doit etre sous $savedDir (recu : $Pipeline)" }
|
||||
$pipeRel = $Pipeline.Substring($savedDir.Length) -replace '\\', '/'
|
||||
if ($pipeRel -match '\s') { throw "-Pipeline ne doit pas contenir d'espace sous Saved\ (recu : $pipeRel)" }
|
||||
$script = "-script=$py mod=$Name src=$Source prefix=$Prefix exclude=$Exclude pipeline=$pipeRel"
|
||||
Write-Host "Livraison dans /$Name/ (journal : $log)"
|
||||
& $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript $script -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Livraison des clips d'animation convertis dans un mod d'assets (lot AP5).
|
||||
# Lancement :
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<chemin>/Tools/anim/DeliverAnims.py mod=<Nom> src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose"
|
||||
# Copie /Game/Quaternius/UAL/B_<Clip> -> /<mod>/Anims/Female/<prefix>_<clip>_F
|
||||
# /Game/Quaternius/UAL/M_<Clip> -> /<mod>/Anims/Male/<prefix>_<clip>_M
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<chemin>/Tools/anim/DeliverAnims.py mod=<Nom> src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose pipeline=AnimPipeline/quaternius"
|
||||
# Copie <src>/B_<Clip> -> /<mod>/Anims/Female/<prefix>_<clip>_F
|
||||
# <src>/M_<Clip> -> /<mod>/Anims/Male/<prefix>_<clip>_M (src = dossier des clips convertis B_/M_)
|
||||
# Les sources ne sont jamais deplacees ni renommees. Seules des copies du mod peuvent etre supprimees.
|
||||
import json
|
||||
import os
|
||||
|
|
@ -12,7 +12,9 @@ import sys
|
|||
import unreal
|
||||
|
||||
# Arguments : mod=... src=... prefix=... exclude=A,B (le script peut recevoir les arguments en un seul bloc)
|
||||
args = {"mod": "lpr_anim_quaternius", "src": "/Game/Quaternius/UAL", "prefix": "qua", "exclude": "A_TPose"}
|
||||
# pipeline= : dossier de livraison.json, relatif a Saved/ (ou absolu) ; sans espace (arguments decoupes sur les espaces).
|
||||
args = {"mod": "lpr_anim_quaternius", "src": "/Game/Quaternius/UAL", "prefix": "qua", "exclude": "A_TPose",
|
||||
"pipeline": "AnimPipeline/quaternius"}
|
||||
tokens = []
|
||||
for a in sys.argv[1:]:
|
||||
tokens.extend(a.split())
|
||||
|
|
@ -89,6 +91,13 @@ for clip in clips:
|
|||
erreur("copie impossible : %s -> %s" % (s, d))
|
||||
ok = False
|
||||
continue
|
||||
# Un mod DLC ne peut pas referencer du contenu moteur non cuit : les clips d'un pack UE5 pointent
|
||||
# explicitement sur /Engine/Animation/DefaultAnim*CompressionSettings (cuisson refusee, AP7). Sans
|
||||
# reglage (None), la compression par defaut du projet s'applique, comme pour les clips Quaternius.
|
||||
for prop in ("bone_compression_settings", "curve_compression_settings"):
|
||||
valeur = copie.get_editor_property(prop)
|
||||
if valeur is not None and valeur.get_path_name().startswith("/Engine/"):
|
||||
copie.set_editor_property(prop, None)
|
||||
if not unreal.EditorAssetLibrary.save_asset(d, only_if_is_dirty=False):
|
||||
erreur("enregistrement impossible : " + d)
|
||||
ok = False
|
||||
|
|
@ -119,7 +128,7 @@ for clip in clips:
|
|||
"images": ref["F"][0], "duree_s": ref["F"][1],
|
||||
})
|
||||
|
||||
sortie = os.path.join(unreal.Paths.project_saved_dir(), "AnimPipeline", "quaternius", "livraison.json")
|
||||
sortie = os.path.join(unreal.Paths.project_saved_dir(), args["pipeline"], "livraison.json") # join : absolu prime
|
||||
os.makedirs(os.path.dirname(sortie), exist_ok=True)
|
||||
with open(sortie, "w", encoding="utf-8") as f:
|
||||
json.dump(livraison, f, indent=2, ensure_ascii=False)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import json
|
|||
import os
|
||||
import sys
|
||||
|
||||
DOSSIER_DEFAUT = r"D:\Game\LifePlay-master 5.5\Saved\AnimPipeline\quaternius"
|
||||
# Defaut = dossier du pack Quaternius ; tout autre pack : passer son dossier en argument.
|
||||
DOSSIER_DEFAUT = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"Saved", "AnimPipeline", "quaternius")
|
||||
|
||||
# (cle CSV valeur, cle CSV os, titre, unite, seuil)
|
||||
MESURES = [
|
||||
|
|
|
|||
463
Tools/anim/RetargetPack.py
Normal file
463
Tools/anim/RetargetPack.py
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
# Recopie les clips d'un pack d'animations sur Bridget et Michael (squelettes Genesis 8 du jeu).
|
||||
# Les chaines portent le meme nom des deux cotes pour que le retarget les aligne.
|
||||
# Les maillages bridget_gp et michael ne sont pas reimportes.
|
||||
# Lot AP7 : script generique (ex-RetargetQuaternius.py), pilote par une methode de transformation
|
||||
# (Tools/anim/methodes/<id>.json ; "ue4" = ancienne methode Quaternius) et par Saved/anim_retarget.json.
|
||||
#
|
||||
# Lance par Tools/BuildMod.ps1 -Retarget (UnrealEditor.exe : la copie a besoin de Slate).
|
||||
# Saved/anim_retarget.json (absent : configuration Quaternius par defaut), champs et defauts :
|
||||
# mode "bake"|"rigs" (defaut "bake" ; a defaut de "mode", Saved/anim_to_genesis_step.txt est lu comme avant),
|
||||
# methode "ue4", source_mesh /Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard, clips_dir /Game/Quaternius/UAL,
|
||||
# out_dir = clips_dir, dest /Game/Quaternius/Retarget (ue4) ou /Game/Retarget_<methode>,
|
||||
# corps [{nom,maillage,prefixe}] Bridget (B_) et Michael (M_), exclure [], pose_dir Saved/Measurement,
|
||||
# jeton (ecrit par BuildMod, ajoute en fin des lignes de resultat et d'echec : "... jeton <jeton>").
|
||||
# Methode factice ou copie, ou profil du maillage source different de profil_source : ligne
|
||||
# "RETARGET echec : <cause>" puis arret.
|
||||
#
|
||||
# Phase C (CHG-204) : la pose de retarget de chaque corps est la pose ajustee sur le mannequin
|
||||
# par la sandbox Mesure (LPPoseMeasure::WriteRetargetPose) : Saved/Measurement/retarget_pose_<Corps>.txt.
|
||||
# Colonne et cou ont plus d'os chez Genesis : chaines Interpolated. Les os Twist restent hors chaine.
|
||||
# AP1 : les chaines et la racine viennent des profils Tools/anim/profiles (identification par les noms d'os).
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"mode": None,
|
||||
"methode": "ue4",
|
||||
"source_mesh": "/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard",
|
||||
"clips_dir": "/Game/Quaternius/UAL",
|
||||
"out_dir": None,
|
||||
"dest": None,
|
||||
"corps": [
|
||||
{"nom": "Bridget", "maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp", "prefixe": "B_"},
|
||||
{"nom": "Michael", "maillage": "/Game/Heavy/Daz/Michael/michael.michael", "prefixe": "M_"},
|
||||
],
|
||||
"exclure": [],
|
||||
"pose_dir": None,
|
||||
"jeton": None,
|
||||
}
|
||||
NONE = unreal.Name()
|
||||
CFG = {}
|
||||
JETON = None
|
||||
|
||||
# Chaines derivees des profils de squelettes (lot AP1) : Tools/anim/profiles/*.json, lus aussi par la
|
||||
# sandbox (LPSkeletonProfile). Une chaine ne traverse jamais un os *Twist (hors chaine, il suit l'os Bend).
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROFILE_DIR = os.path.join(HERE, "profiles")
|
||||
METHOD_DIR = os.path.join(HERE, "methodes")
|
||||
SIDES = (("l", "Left"), ("r", "Right"))
|
||||
FINGERS = (("thumb", "Thumb"), ("index", "Index"), ("middle", "Mid"), ("ring", "Ring"), ("pinky", "Pinky"))
|
||||
|
||||
|
||||
class Refusal(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def load_config(saved_dir):
|
||||
# Configuration par defaut = comportement Quaternius ; Saved/anim_retarget.json la complete champ par champ.
|
||||
config = json.loads(json.dumps(DEFAULT_CONFIG))
|
||||
path = os.path.join(saved_dir, "anim_retarget.json")
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8-sig") as handle:
|
||||
given = json.load(handle)
|
||||
global JETON
|
||||
JETON = str(given["jeton"]) if given.get("jeton") else None
|
||||
for key, value in given.items():
|
||||
if key not in DEFAULT_CONFIG:
|
||||
raise Refusal("champ inconnu %s dans %s" % (key, path))
|
||||
config[key] = value
|
||||
if config["mode"] is None:
|
||||
config["mode"] = "bake"
|
||||
legacy = os.path.join(saved_dir, "anim_to_genesis_step.txt")
|
||||
if os.path.exists(legacy):
|
||||
with open(legacy, "r", encoding="utf-8") as handle:
|
||||
config["mode"] = handle.read().strip().lower() or "bake"
|
||||
config["mode"] = str(config["mode"]).strip().lower()
|
||||
if config["mode"] not in ("bake", "rigs"):
|
||||
raise Refusal("etape inconnue %s" % config["mode"])
|
||||
config["clips_dir"] = config["clips_dir"].rstrip("/")
|
||||
config["out_dir"] = (config["out_dir"] or config["clips_dir"]).rstrip("/")
|
||||
if not config["dest"]:
|
||||
config["dest"] = "/Game/Quaternius/Retarget" if config["methode"] == "ue4" else "/Game/Retarget_%s" % config["methode"]
|
||||
config["dest"] = config["dest"].rstrip("/")
|
||||
if not config["pose_dir"]:
|
||||
config["pose_dir"] = os.path.join(saved_dir, "Measurement")
|
||||
if not config["corps"]:
|
||||
raise Refusal("aucun corps dans la configuration")
|
||||
return config
|
||||
|
||||
|
||||
def load_method(method_id, method_dir=None):
|
||||
path = os.path.join(method_dir or METHOD_DIR, "%s.json" % method_id)
|
||||
if not os.path.exists(path):
|
||||
raise Refusal("methode inconnue %s" % method_id)
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
method = json.load(handle)
|
||||
if method.get("factice") or method.get("copie"):
|
||||
raise Refusal("methode %s non implementee (factice ou copie)" % method_id)
|
||||
return method
|
||||
|
||||
|
||||
def suffix(config):
|
||||
# Les assets de la methode ue4 gardent leurs noms historiques ; les autres sont suffixes par la methode.
|
||||
return "" if config["methode"] == "ue4" else "_" + config["methode"]
|
||||
|
||||
|
||||
def source_rig_name(config):
|
||||
return "IK_Quaternius" if config["methode"] == "ue4" else "IK_Source" + suffix(config)
|
||||
|
||||
|
||||
def load_profiles():
|
||||
profiles = []
|
||||
for name in sorted(os.listdir(PROFILE_DIR)):
|
||||
if name.endswith(".json"):
|
||||
with open(os.path.join(PROFILE_DIR, name), "r", encoding="utf-8") as handle:
|
||||
profiles.append(json.load(handle))
|
||||
if not profiles:
|
||||
raise RuntimeError("aucun profil de squelette dans %s" % PROFILE_DIR)
|
||||
return profiles
|
||||
|
||||
|
||||
def role(profile, name, index=0):
|
||||
value = profile["roles"].get(name)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
return value[index] if -len(value) <= index < len(value) else None
|
||||
return value if index in (0, -1) else None
|
||||
|
||||
|
||||
def identify_names(names, label="?"):
|
||||
# Meme regle que LPSkeletonProfile::Identify : au moins 95 % des os du profil presents ; a egalite de
|
||||
# score, le profil qui reconnait le plus d'os (le mannequin UE5 contient tous les noms d'os UE4).
|
||||
names = set(names)
|
||||
best, best_score, best_hits = None, 0.0, 0
|
||||
for profile in load_profiles():
|
||||
bones = []
|
||||
for value in profile["roles"].values():
|
||||
bones.extend(value if isinstance(value, list) else [value])
|
||||
hits = sum(1 for bone in bones if bone in names)
|
||||
score = round(hits / float(len(bones)), 6)
|
||||
if score > best_score or (score == best_score and hits > best_hits):
|
||||
best, best_score, best_hits = profile, score, hits
|
||||
if best is None or best_score < 0.95:
|
||||
raise RuntimeError("squelette inconnu %s (meilleur profil %.0f%%)" % (label, best_score * 100.0))
|
||||
return best, best_score
|
||||
|
||||
|
||||
def identify(mesh):
|
||||
best, best_score = identify_names(bone_names(mesh), mesh.get_name())
|
||||
unreal.log_warning("RETARGET profil %s -> %s (%.0f%%)" % (mesh.get_name(), best["id"], best_score * 100.0))
|
||||
return best
|
||||
|
||||
|
||||
def chains(profile):
|
||||
# (nom de chaine, premier os, dernier os) : memes noms des deux cotes pour auto_map_chains.
|
||||
made = [("Spine", role(profile, "spine", 0), role(profile, "spine", -1)),
|
||||
("Neck", role(profile, "neck", 0), role(profile, "head"))]
|
||||
for side, word in SIDES:
|
||||
made += [
|
||||
(word + "Collar", role(profile, "clavicle_" + side), role(profile, "upperarm_" + side)),
|
||||
(word + "Forearm", role(profile, "lowerarm_" + side), role(profile, "lowerarm_" + side)),
|
||||
(word + "Hand", role(profile, "hand_" + side), role(profile, "hand_" + side)),
|
||||
(word + "Thigh", role(profile, "thigh_" + side), role(profile, "thigh_" + side)),
|
||||
(word + "Calf", role(profile, "calf_" + side), role(profile, "calf_" + side)),
|
||||
(word + "Foot", role(profile, "foot_" + side), role(profile, "foot_" + side)),
|
||||
(word + "Toe", role(profile, "toe_" + side), role(profile, "toe_" + side)),
|
||||
]
|
||||
for finger, daz in FINGERS:
|
||||
made.append((word + daz, role(profile, finger + "_" + side, 0), role(profile, finger + "_" + side, -1)))
|
||||
missing = [name for name, start, end in made if not start or not end]
|
||||
if missing:
|
||||
raise RuntimeError("profil %s : chaines sans os %s" % (profile["id"], ", ".join(missing)))
|
||||
return made
|
||||
|
||||
|
||||
INTERPOLATED = ("Spine", "Neck")
|
||||
|
||||
|
||||
def make_factory(factory_class):
|
||||
factory = factory_class()
|
||||
factory.set_editor_property("edit_after_new", False)
|
||||
return factory
|
||||
|
||||
|
||||
def vsub(a, b):
|
||||
return unreal.MathLibrary.subtract_vector_vector(a, b)
|
||||
|
||||
|
||||
def vnorm(v):
|
||||
return unreal.MathLibrary.normal(v)
|
||||
|
||||
|
||||
def vdot(a, b):
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
|
||||
|
||||
def vcross(a, b):
|
||||
return unreal.MathLibrary.cross_vector_vector(a, b)
|
||||
|
||||
|
||||
def bone_names(mesh):
|
||||
pose = mesh.skeleton.get_reference_pose()
|
||||
return [str(name) for name in unreal.AnimPoseExtensions.get_bone_names(pose)]
|
||||
|
||||
|
||||
def pose_file(body):
|
||||
return os.path.join(CFG["pose_dir"], "retarget_pose_%s.txt" % body)
|
||||
|
||||
|
||||
def read_pose(body):
|
||||
path = pose_file(body)
|
||||
if not os.path.exists(path):
|
||||
raise RuntimeError("pose de retarget absente %s : ouvrir la sandbox Mesure une fois" % path)
|
||||
offsets = []
|
||||
with open(path, "r", encoding="utf-8-sig") as handle:
|
||||
for line in handle:
|
||||
cells = line.split()
|
||||
if len(cells) != 5 or cells[0].startswith("#"):
|
||||
continue
|
||||
offsets.append((cells[0], unreal.Quat(float(cells[1]), float(cells[2]), float(cells[3]), float(cells[4]))))
|
||||
return offsets
|
||||
|
||||
|
||||
def apply_pose(ctrl, mesh, body):
|
||||
side = unreal.RetargetSourceOrTarget.TARGET
|
||||
names = set(bone_names(mesh))
|
||||
applied = 0
|
||||
for bone, quat in read_pose(body):
|
||||
if bone not in names:
|
||||
unreal.log_warning("RETARGET pose %s os absent %s" % (body, bone))
|
||||
continue
|
||||
ctrl.set_rotation_offset_for_retarget_pose_bone(bone, quat, side)
|
||||
applied += 1
|
||||
unreal.log_warning("RETARGET pose %s %d os (pose %s ajustee)" % (body, applied, CFG["pose"]))
|
||||
|
||||
|
||||
def make_rig(name, mesh_path):
|
||||
asset_path = CFG["dest"] + "/" + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
||||
unreal.EditorAssetLibrary.delete_asset(asset_path)
|
||||
mesh = unreal.load_asset(mesh_path)
|
||||
profile = identify(mesh)
|
||||
root = role(profile, "hips")
|
||||
rig_chains = chains(profile)
|
||||
rig = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
name, CFG["dest"], unreal.IKRigDefinition, make_factory(unreal.IKRigDefinitionFactory))
|
||||
ctrl = unreal.IKRigController.get_controller(rig)
|
||||
ctrl.set_skeletal_mesh(mesh)
|
||||
ctrl.set_retarget_root(root)
|
||||
for chain, start, end in rig_chains:
|
||||
made = str(ctrl.add_retarget_chain(chain, start, end, NONE))
|
||||
unreal.log_warning("RETARGET chaine %s %s %s -> %s" % (name, chain, start, made))
|
||||
if made == "None":
|
||||
raise RuntimeError("chaine impossible %s %s %s" % (name, chain, start))
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(rig)
|
||||
return rig, mesh, rig_chains
|
||||
|
||||
|
||||
def make_retargeter(name, source_rig, target_rig, source_mesh, target_mesh, body, source_chains):
|
||||
asset_path = CFG["dest"] + "/" + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
||||
unreal.EditorAssetLibrary.delete_asset(asset_path)
|
||||
retargeter = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
name, CFG["dest"], unreal.IKRetargeter, make_factory(unreal.IKRetargetFactory))
|
||||
ctrl = unreal.IKRetargeterController.get_controller(retargeter)
|
||||
ctrl.set_ik_rig(unreal.RetargetSourceOrTarget.SOURCE, source_rig)
|
||||
ctrl.set_ik_rig(unreal.RetargetSourceOrTarget.TARGET, target_rig)
|
||||
ctrl.set_preview_mesh(unreal.RetargetSourceOrTarget.SOURCE, source_mesh)
|
||||
ctrl.set_preview_mesh(unreal.RetargetSourceOrTarget.TARGET, target_mesh)
|
||||
ctrl.auto_map_chains(unreal.AutoMapChainType.EXACT, True)
|
||||
# Pose de retarget : la pose ajustee de la sandbox, et non plus l'alignement par le maillage,
|
||||
# les reglages Daz et les demi-tours d'epaule (pole / roll180), qu'elle remplace.
|
||||
apply_pose(ctrl, target_mesh, body)
|
||||
for chain_name, _, _ in source_chains:
|
||||
settings = ctrl.get_retarget_chain_settings(chain_name)
|
||||
fk = settings.get_editor_property("fk")
|
||||
mode = unreal.RetargetRotationMode.INTERPOLATED if chain_name in INTERPOLATED else unreal.RetargetRotationMode.ONE_TO_ONE
|
||||
fk.set_editor_property("rotation_mode", mode)
|
||||
fk.set_editor_property("translation_mode", unreal.RetargetTranslationMode.NONE)
|
||||
fk.set_editor_property("pole_vector_matching", 0.0)
|
||||
settings.set_editor_property("fk", fk)
|
||||
ctrl.set_retarget_chain_settings(chain_name, settings)
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(retargeter)
|
||||
return retargeter
|
||||
|
||||
|
||||
def source_clips():
|
||||
prefixes = tuple(body["prefixe"] for body in CFG["corps"])
|
||||
clips = []
|
||||
for path in unreal.EditorAssetLibrary.list_assets(CFG["clips_dir"], False, False):
|
||||
asset = unreal.load_asset(path)
|
||||
if not isinstance(asset, unreal.AnimSequence):
|
||||
continue
|
||||
name = asset.get_name()
|
||||
if name.startswith(prefixes) or name in CFG["exclure"]:
|
||||
continue
|
||||
clips.append(unreal.EditorAssetLibrary.find_asset_data(path))
|
||||
return clips
|
||||
|
||||
|
||||
def delete_copies(names):
|
||||
# Seulement les copies des clips source. Pas tout asset dont le nom commence par un prefixe de corps.
|
||||
for name in names:
|
||||
for body in CFG["corps"]:
|
||||
for folder in (CFG["out_dir"] + "/", "/Game/"):
|
||||
path = folder + body["prefixe"] + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
|
||||
|
||||
def tag(line):
|
||||
# AP7 : BuildMod ne retient que les lignes de resultat portant son jeton (cle "jeton" de anim_retarget.json).
|
||||
return "%s jeton %s" % (line, JETON) if JETON else line
|
||||
|
||||
|
||||
def finish(line):
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
unreal.log_warning(tag(line))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
|
||||
|
||||
def anim_pose(sequence, time):
|
||||
try:
|
||||
options = unreal.AnimPoseEvaluationOptions()
|
||||
for prop in ("should_retarget", "b_should_retarget"):
|
||||
try:
|
||||
options.set_editor_property(prop, False)
|
||||
except Exception:
|
||||
pass
|
||||
return unreal.AnimPoseExtensions.get_anim_pose_at_time(sequence, time, options)
|
||||
except Exception as exc:
|
||||
unreal.log_warning("RETARGET pose api %s" % exc)
|
||||
return unreal.AnimPoseExtensions.get_anim_pose_at_time(sequence, time)
|
||||
|
||||
|
||||
def hand_ahead(sequence, chest_name, hand_name, left_name, right_name, time, pelvis_name="pelvis"):
|
||||
pose = anim_pose(sequence, time)
|
||||
space = unreal.AnimPoseSpaces.WORLD
|
||||
|
||||
def loc(bone):
|
||||
return unreal.AnimPoseExtensions.get_bone_pose(pose, bone, space).translation
|
||||
|
||||
chest = loc(chest_name)
|
||||
pelvis = loc(pelvis_name)
|
||||
up = vnorm(vsub(chest, pelvis))
|
||||
side = vnorm(vsub(loc(left_name), loc(right_name)))
|
||||
forward = vnorm(vcross(side, up))
|
||||
return vdot(vsub(loc(hand_name), chest), forward)
|
||||
|
||||
|
||||
def log_hands(label):
|
||||
# Releve seulement : main devant (+) ou derriere (-) le buste sur le premier clip cuit du pack.
|
||||
# La mesure fine est la tournee de la sandbox. Sans clip cuit ou sans roles : on saute.
|
||||
profile = CFG["source_profile"]
|
||||
chest_src = role(profile, "spine", -1)
|
||||
hips_src = role(profile, "hips")
|
||||
hands_src = (role(profile, "hand_l"), role(profile, "hand_r"))
|
||||
collars_src = (role(profile, "clavicle_l"), role(profile, "clavicle_r"))
|
||||
if not (chest_src and hips_src and all(hands_src) and all(collars_src)):
|
||||
unreal.log_warning("RETARGET mesure sautee : roles du profil source incomplets")
|
||||
return
|
||||
first = None
|
||||
for body in CFG["corps"]:
|
||||
for data in source_clips():
|
||||
name = str(data.asset_name)
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(CFG["out_dir"] + "/" + body["prefixe"] + name):
|
||||
first = name
|
||||
break
|
||||
if first:
|
||||
break
|
||||
if first is None:
|
||||
unreal.log_warning("RETARGET mesure sautee : aucun clip cuit")
|
||||
return
|
||||
spec = [(CFG["clips_dir"] + "/" + first, chest_src, hips_src, hands_src, collars_src)]
|
||||
for body in CFG["corps"]:
|
||||
spec.append((CFG["out_dir"] + "/" + body["prefixe"] + first, "chestUpper", "pelvis", ("lHand", "rHand"), ("lCollar", "rCollar")))
|
||||
for path, chest, pelvis, hands, collars in spec:
|
||||
leaf = path.split("/")[-1]
|
||||
sequence = unreal.load_asset(path + "." + leaf)
|
||||
if not sequence:
|
||||
unreal.log_warning("RETARGET mesure absente %s" % path)
|
||||
continue
|
||||
for hand in hands:
|
||||
ahead = hand_ahead(sequence, chest, hand, collars[0], collars[1], 0.0, pelvis)
|
||||
unreal.log_warning("RETARGET main %s %s %s devant %.1f" % (label, leaf, hand, ahead))
|
||||
|
||||
|
||||
def bake_named(clips, source_mesh, bodies):
|
||||
# bodies : [(corps, maillage, retargeter)] ; renvoie (nombre de copies par corps, nombre deplaces).
|
||||
made = []
|
||||
for body, mesh, retargeter in bodies:
|
||||
made.append(unreal.IKRetargetBatchOperation.duplicate_and_retarget(
|
||||
clips, source_mesh, mesh, retargeter, "", "", body["prefixe"], "", True))
|
||||
moved = 0
|
||||
for data in [item for group in made for item in group]:
|
||||
package = str(data.package_name)
|
||||
name = str(data.asset_name)
|
||||
dest = CFG["out_dir"] + "/" + name
|
||||
if package == dest or unreal.EditorAssetLibrary.rename_asset(package, dest):
|
||||
moved += 1
|
||||
else:
|
||||
unreal.log_error("RETARGET deplacement rate %s" % package)
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
return [len(group) for group in made], moved
|
||||
|
||||
|
||||
def run():
|
||||
global CFG
|
||||
CFG = load_config(unreal.Paths.project_saved_dir())
|
||||
method = load_method(CFG["methode"])
|
||||
CFG["pose"] = method["pose"]
|
||||
mode = CFG["mode"]
|
||||
unreal.log_warning("RETARGET etape %s" % mode)
|
||||
suf = suffix(CFG)
|
||||
probe = unreal.load_asset(CFG["source_mesh"])
|
||||
if not probe:
|
||||
raise Refusal("maillage source introuvable %s" % CFG["source_mesh"])
|
||||
source_profile, _ = identify_names(bone_names(probe), probe.get_name())
|
||||
if source_profile["id"] != method["profil_source"]:
|
||||
raise Refusal("profil du maillage source %s (%s) different de profil_source %s de la methode %s" % (
|
||||
probe.get_name(), source_profile["id"], method["profil_source"], CFG["methode"]))
|
||||
CFG["source_profile"] = source_profile
|
||||
unreal.log_warning("RETARGET methode %s profil %s calques %s" % (CFG["methode"], source_profile["id"], method["pose"]))
|
||||
source_rig, source_mesh, source_chains = make_rig(source_rig_name(CFG), CFG["source_mesh"])
|
||||
bodies = []
|
||||
for body in CFG["corps"]:
|
||||
rig, mesh, _ = make_rig("IK_" + body["nom"] + suf, body["maillage"])
|
||||
retargeter = make_retargeter("RTG_" + body["nom"] + suf, source_rig, rig, source_mesh, mesh, body["nom"], source_chains)
|
||||
bodies.append((body, mesh, retargeter))
|
||||
if mode == "rigs":
|
||||
finish("RETARGET rigs prets")
|
||||
return
|
||||
names = [str(data.asset_name) for data in source_clips()]
|
||||
delete_copies(names)
|
||||
clips = source_clips()
|
||||
unreal.log_warning("RETARGET clips source %d" % len(clips))
|
||||
counts, moved = bake_named(clips, source_mesh, bodies)
|
||||
try:
|
||||
log_hands("final")
|
||||
except Exception as exc:
|
||||
unreal.log_warning("RETARGET mesure finale ratee %s" % exc)
|
||||
done = " ".join("%s %d" % (body["nom"].lower(), count) for (body, _, _), count in zip(bodies, counts))
|
||||
finish("RETARGET %s deplaces %d pose %s ajustee" % (done, moved, method["pose"]))
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
run()
|
||||
except Refusal as exc:
|
||||
unreal.log_warning(tag("RETARGET echec : %s" % exc))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
except Exception as exc:
|
||||
unreal.log_warning(tag("RETARGET echec : %s" % exc))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
raise
|
||||
|
||||
|
||||
if not os.environ.get("LP_RETARGET_NOMAIN"):
|
||||
main()
|
||||
12
Tools/anim/methodes/genesis_direct.json
Normal file
12
Tools/anim/methodes/genesis_direct.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "genesis_direct",
|
||||
"nom": "Genesis 8 vers Genesis 8 (copie)",
|
||||
"profil_source": "genesis8",
|
||||
"pose": "A",
|
||||
"copie": true,
|
||||
"factice": true,
|
||||
"description": "Source Daz (DUF) sur le meme squelette que le corps : copie sans transformation. Methode declaree, non implementee : la chaine la refuse avec un message clair.",
|
||||
"exemples": [
|
||||
"poses et animations Daz Studio (.duf)"
|
||||
]
|
||||
}
|
||||
10
Tools/anim/methodes/ue4.json
Normal file
10
Tools/anim/methodes/ue4.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "ue4",
|
||||
"nom": "Mannequin UE4 vers Genesis 8 (T vers T)",
|
||||
"profil_source": "ue4_mannequin",
|
||||
"pose": "T",
|
||||
"description": "Le mannequin UE4 est livre en pose T : calque d'origine = repos du mannequin ; calque du corps = repos Genesis + reglages Daz A vers T du profil genesis8, ajuste a la morphologie (phase A). Ancienne methode 'Quaternius'.",
|
||||
"exemples": [
|
||||
"Quaternius Universal Animation Library"
|
||||
]
|
||||
}
|
||||
10
Tools/anim/methodes/ue5.json
Normal file
10
Tools/anim/methodes/ue5.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "ue5",
|
||||
"nom": "Mannequin UE5 vers Genesis 8 (A vers A)",
|
||||
"profil_source": "ue5_mannequin",
|
||||
"pose": "A",
|
||||
"description": "Le mannequin UE5 est livre en pose A, comme Bridget et Michael : calque d'origine = repos du mannequin ; calque du corps = repos Genesis (A), ajuste a la morphologie (phase A). Aucun reglage A vers T.",
|
||||
"exemples": [
|
||||
"FreeAnimationsPack (SK_Mannequin Manny / Quinn)"
|
||||
]
|
||||
}
|
||||
129
Tools/anim/profiles/ue5_mannequin.json
Normal file
129
Tools/anim/profiles/ue5_mannequin.json
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"id": "ue5_mannequin",
|
||||
"nom": "Mannequin UE5 (SK_Mannequin Manny / Quinn) : FreeAnimationsPack et packs Epic UE5",
|
||||
"description": "Repos en pose A (bras 54,8 deg sous l'horizontale). Colonne a 5 os, cou a 2 os. Os de torsion, metacarpes et correctifs hors roles (pilotes, non transferes). Roles : un nom d'os, ou une liste ordonnee de la racine vers le bout pour une chaine.",
|
||||
"pose_t": {
|
||||
"type": "reglages",
|
||||
"source": "Reglages A vers T utilises seulement par une methode en pose T (comparaison) ; la methode ue5 (pose A) prend le repos tel quel. repos SK_Mannequin releve par -run=LPAnimIdentify (AP7) ; cibles = pose T du mannequin Quaternius (clavicule 4,42 deg sous l'horizontale, cuisse 0,29 deg) ; reglages cumules du parent a l'enfant (bras 54,80 - 4,29)",
|
||||
"reglages": [
|
||||
{
|
||||
"os": "clavicle_l",
|
||||
"enfant": "upperarm_l",
|
||||
"cible": "plat",
|
||||
"degres": 4.29
|
||||
},
|
||||
{
|
||||
"os": "clavicle_r",
|
||||
"enfant": "upperarm_r",
|
||||
"cible": "plat",
|
||||
"degres": 4.29
|
||||
},
|
||||
{
|
||||
"os": "upperarm_l",
|
||||
"enfant": "lowerarm_l",
|
||||
"cible": "plat",
|
||||
"degres": 50.51
|
||||
},
|
||||
{
|
||||
"os": "upperarm_r",
|
||||
"enfant": "lowerarm_r",
|
||||
"cible": "plat",
|
||||
"degres": 50.51
|
||||
},
|
||||
{
|
||||
"os": "thigh_l",
|
||||
"enfant": "calf_l",
|
||||
"cible": "bas",
|
||||
"degres": 2.76
|
||||
},
|
||||
{
|
||||
"os": "thigh_r",
|
||||
"enfant": "calf_r",
|
||||
"cible": "bas",
|
||||
"degres": 2.76
|
||||
}
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"hips": "pelvis",
|
||||
"pelvis": "pelvis",
|
||||
"spine": [
|
||||
"spine_01",
|
||||
"spine_02",
|
||||
"spine_03",
|
||||
"spine_04",
|
||||
"spine_05"
|
||||
],
|
||||
"neck": [
|
||||
"neck_01",
|
||||
"neck_02"
|
||||
],
|
||||
"head": "head",
|
||||
"clavicle_l": "clavicle_l",
|
||||
"upperarm_l": "upperarm_l",
|
||||
"lowerarm_l": "lowerarm_l",
|
||||
"hand_l": "hand_l",
|
||||
"thumb_l": [
|
||||
"thumb_01_l",
|
||||
"thumb_02_l",
|
||||
"thumb_03_l"
|
||||
],
|
||||
"index_l": [
|
||||
"index_01_l",
|
||||
"index_02_l",
|
||||
"index_03_l"
|
||||
],
|
||||
"middle_l": [
|
||||
"middle_01_l",
|
||||
"middle_02_l",
|
||||
"middle_03_l"
|
||||
],
|
||||
"ring_l": [
|
||||
"ring_01_l",
|
||||
"ring_02_l",
|
||||
"ring_03_l"
|
||||
],
|
||||
"pinky_l": [
|
||||
"pinky_01_l",
|
||||
"pinky_02_l",
|
||||
"pinky_03_l"
|
||||
],
|
||||
"thigh_l": "thigh_l",
|
||||
"calf_l": "calf_l",
|
||||
"foot_l": "foot_l",
|
||||
"toe_l": "ball_l",
|
||||
"clavicle_r": "clavicle_r",
|
||||
"upperarm_r": "upperarm_r",
|
||||
"lowerarm_r": "lowerarm_r",
|
||||
"hand_r": "hand_r",
|
||||
"thumb_r": [
|
||||
"thumb_01_r",
|
||||
"thumb_02_r",
|
||||
"thumb_03_r"
|
||||
],
|
||||
"index_r": [
|
||||
"index_01_r",
|
||||
"index_02_r",
|
||||
"index_03_r"
|
||||
],
|
||||
"middle_r": [
|
||||
"middle_01_r",
|
||||
"middle_02_r",
|
||||
"middle_03_r"
|
||||
],
|
||||
"ring_r": [
|
||||
"ring_01_r",
|
||||
"ring_02_r",
|
||||
"ring_03_r"
|
||||
],
|
||||
"pinky_r": [
|
||||
"pinky_01_r",
|
||||
"pinky_02_r",
|
||||
"pinky_03_r"
|
||||
],
|
||||
"thigh_r": "thigh_r",
|
||||
"calf_r": "calf_r",
|
||||
"foot_r": "foot_r",
|
||||
"toe_r": "ball_r"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue