DC2 : commandlets de mods dans le module LPModTools de l'addon ; le jeu compile et demarre sans l'addon (CHG-235)
13 sources retirees de LifePlayEditorTools (LPAnimEval/Twist/Catalog/Identify, LPAnimChecks, LPAnimMethodSelect, LPHairFromStaticMesh) ; LifePlayEditorTools.Build.cs ne depend plus de LPAnimMeasure. Pin addon 97c0d13. Validation : DC2-BUILD avec et sans addon code 0, DC2-VAL PASS (resultats identiques a SB2-REF, jeu -game sans addon). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d8a0f859ce
commit
bd4ea43d20
24 changed files with 299 additions and 2647 deletions
|
|
@ -18,7 +18,7 @@
|
|||
| Cache de données dérivées | `D:\DDC` (à conserver : évite des heures de recompilation de shaders) |
|
||||
| Dépôt public | `github.com/edujiro5972/LifePlay-Revival` (branche `main` ; `ue5.5-v1.1` = V1.1) |
|
||||
|
||||
Python 3 est utile pour `Tools/DiffRemplacementNoeuds.py`. PowerShell 7 pour les scripts `Tools/*.ps1`.
|
||||
Python 3 est utile pour `Tools/DiffRemplacementNoeuds.py`. PowerShell 7 pour les scripts `Tools/*.ps1`. L'addon `LifePlay-Sandbox` n'est plus requis pour compiler ni lancer le jeu ; il fournit la sandbox et l'atelier de mods (module `LPModTools`).
|
||||
|
||||
## 2. Arborescence
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on
|
|||
|
||||
# Coiffure : import FBX, conversion, matériaux (protocole complet : 13_Protocole_Coiffure.md)
|
||||
& $Cmd $Proj -run=pythonscript "-script=<projet>/Tools/HairImportFbx.py fbx=<f.fbx> dest=/Game/Heavy/New5/Hairs/_Work" -unattended -nopause
|
||||
# exige l'addon LifePlay-Sandbox (module LPModTools) ; sans lui : « LPHairFromStaticMeshCommandlet looked like a commandlet, but we could not find the class »
|
||||
& $Cmd $Proj -run=LPHairFromStaticMesh -Static=<maillage> -FitHead=<tete> -Out=/Game/Heavy/New5/Hairs/<Nom> -DryRun -unattended -nopause
|
||||
& $Cmd $Proj -run=pythonscript "-script=<projet>/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/<Nom> diffuse=<f> strands_opacity=<f> cap_opacity=white cap=<emplacement>" -unattended -nopause
|
||||
|
||||
|
|
@ -98,7 +99,7 @@ Prérequis : clone `LifePlay-Sandbox` à côté du jeu, pin et liaison décrits
|
|||
|
||||
Sorties du mode Mesure (relatif à `Saved/`) : `Measurement/retarget_pose_<Corps>.txt`, `Measurement/tour.csv`, `Measurement/tour/`, `Measurement/vignettes/`, `Measurement/planches/`, etc. (`Config/LPSandbox.ini`, clé `MeasureOutputRoot`).
|
||||
|
||||
Chaîne d'animations headless (`LifePlayEditorTools`, module `LPAnimMeasure` fourni par le plugin) :
|
||||
Chaîne d'animations headless (module `LPModTools` de l'addon `LifePlay-Sandbox`, lot DC2 ; sans l'addon : « LPAnimEvalCommandlet looked like a commandlet, but we could not find the class ») :
|
||||
|
||||
```powershell
|
||||
# Évaluation courte (1 clip) — archivage journal : RunCommandlet.ps1
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Rien n'est supprimé : un résultat précédent est déplacé dans `precedent_<d
|
|||
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é. Depuis le lot DC1, `LPAnimRedirect` et `LPCoupleSim` sont dans le plugin `LPAnimCore` du jeu (`Plugins/LPAnimCore`) ; la mesure reste dans le module `LPAnimMeasure` de l'addon.
|
||||
`LPAnimRedirect` ne sert plus qu'à la sandbox et au personnage piloté. Depuis le lot DC1, `LPAnimRedirect` et `LPCoupleSim` sont dans le plugin `LPAnimCore` du jeu (`Plugins/LPAnimCore`) ; la mesure reste dans le module `LPAnimMeasure` de l'addon. Depuis le lot DC2, les commandlets `LPAnimEval`, `LPAnimTwist`, `LPAnimCatalog` et `LPAnimIdentify` sont dans le module éditeur `LPModTools` de l'addon ; profils et méthodes sont lus via `LPAnimMeasurePaths` (addon, repli sur le jeu jusqu'à DC3).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ Le lot S2 sort la chaîne de mesure de `LifePlay` vers un module Runtime dédié
|
|||
```mermaid
|
||||
flowchart LR
|
||||
LPET["LifePlayEditorTools<br/>(Editor)"] -->|Public| LP["LifePlay<br/>(Runtime, module primaire)"]
|
||||
LPET -->|Public<br/>FLPPose dans LPAnimChecks.h| AM["LPAnimMeasure<br/>(Runtime, nouveau S2)"]
|
||||
MT["LPModTools<br/>(Editor, addon, DC2)"] -->|Public| AM["LPAnimMeasure<br/>(Runtime, nouveau S2)"]
|
||||
LP -->|Public<br/>LPPairSandbox, LPDirectControl| AM
|
||||
LP -->|Public DC1| AC["LPAnimCore<br/>(plugin du jeu : LPAnimRedirect, LPCoupleSim)"]
|
||||
AM -->|Private DC1| AC
|
||||
|
|
@ -104,7 +104,7 @@ Règles : `LPAnimMeasure` ne dépend ni de `LifePlay` ni de `LifePlayEditorTools
|
|||
| `LPAnimMeasure` | `LPAnimCore` | Private | DC1 : la mesure utilise `LPAnimRedirect` |
|
||||
| `LPSandbox` | `LPAnimMeasure`, `LPAnimCore` | Public | DC1 : mesure et redirection / géométrie du couple |
|
||||
| `LifePlayEditorTools` | `LifePlay` | Public | existant |
|
||||
| `LifePlayEditorTools` | `LPAnimMeasure` | Public | `LPAnimChecks.h` expose `FLPPose` ; commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` (jusqu'à DC2) |
|
||||
| `LPModTools` (Editor, addon) | `LPAnimMeasure` | Public | DC2 : `LPAnimChecks.h` expose `FLPPose` ; commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist`, `LPAnimIdentify`, `LPHairFromStaticMesh` (ex-`LifePlayEditorTools`, qui ne dépend plus de `LPAnimMeasure`) |
|
||||
| `LPAnimMeasure` | `Core`, `CoreUObject`, `Engine` / `Json` | Public / Privé | animations, squelettes ; lecture de `Tools/anim/profiles/*.json` |
|
||||
| `LPAnimMeasure` | `LifePlay`, `LifePlayEditorTools` | **aucune** | évite un cycle de modules |
|
||||
|
||||
|
|
@ -124,12 +124,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 `3e9c32f1f27b1abdaf7731dc90496c27ee061718`, `lot/DC1-animcore`, DC1 `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu ; précédents : `16905c68070992f5139dab907b41b461fb765957` SB4 ouverture par déclencheur éphémère non mémorisée, `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `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 :
|
||||
- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `97c0d134cd1388c3735add79d77ea98817e89fbe`, `lot/DC2-modtools`, DC2 module éditeur `LPModTools` et chemins propres à l'addon ; précédents : `3e9c32f1f27b1abdaf7731dc90496c27ee061718` DC1 `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `16905c68070992f5139dab907b41b461fb765957` SB4 ouverture par déclencheur éphémère non mémorisée, `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `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 3e9c32f1f27b1abdaf7731dc90496c27ee061718
|
||||
git -C LifePlay-Sandbox checkout 97c0d134cd1388c3735add79d77ea98817e89fbe
|
||||
# le jeu doit être cloné à côté : D:\Game\LifePlay-master 5.5
|
||||
```
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ Le lot S5 ferme la **dérive de fork** entre la sandbox (`LPSandbox/LPCoupleSim.
|
|||
- [x] **S7 / isolation** : plugin sans `include` / `LIFEPLAY_API` / module `LifePlay` ; `Person` = CDO seul, aucun `GI` / `Room` ; chemins par config (`S7-SPEC.md`, `S7-CART.md` : 0 écart).
|
||||
- [x] **S7 / configurations** : `LPSandbox` hors Shipping (`TargetConfigurationDenyList`), `LPAnimMeasure` conservé, plugin `Optional` dans `LifePlay.uproject` (`S7-IMPL-L.md`).
|
||||
- [x] **Smoke Pair** (bat court, plugin monté, boîte du couple identique à S5-VAL, 0 crash) (`S7-VAL.md`).
|
||||
- [ ] Commandlets anim restants (`LPAnimCatalog`, `LPAnimChecks`, `LPAnimTwist`) déplacés dans un module éditeur de l'addon : **non fait** (restent dans `LifePlayEditorTools`).
|
||||
- [x] Commandlets anim restants (`LPAnimCatalog`, `LPAnimChecks`, `LPAnimTwist`) déplacés dans un module éditeur de l'addon : **fait au lot DC2** (module `LPModTools`, avec `LPAnimEval`, `LPAnimIdentify`, `LPAnimMethodSelect`, `LPHairFromStaticMesh`).
|
||||
- [x] **S8** : bilan du mode multi-agents (`22_Eval_Agents_Sandbox.md` : GO conditionnel) ; code sandbox absent du module jeu depuis S3 ; smoke Pair par le `.bat` réel, boîte du couple identique à S5 / S7, 0 crash (`S8-VAL.md` PASS).
|
||||
- [ ] `BuildMod.ps1 -Target sandbox -Open` rejoué sur la liaison addon : **non fait**.
|
||||
- [ ] Compilation / packaging **Shipping** complets avec le plugin (seul le plan UBT est vérifié).
|
||||
|
|
@ -187,11 +187,14 @@ Le lot S5 ferme la **dérive de fork** entre la sandbox (`LPSandbox/LPCoupleSim.
|
|||
- [x] Reports SB2 : (O1) traité (SB2-O1, `3d858527fccfdddd8711c7c7448fa001442e0b8a` : `ShowRoom` symétrique de `HideRoom`, contrôle visuel utilisateur 2026-10-07 « c'est bon ») ; (E3) sans `lps_lib_scene`, la pièce est construite quand même : non traité, séparation physique abandonnée à la porte G-SB3 (décision utilisateur 2026-10-07).
|
||||
- [x] **DC0** : cartographie et SPEC du découplage jeu / sandbox (`etat/sandbox/DC0/DC0-SPEC.md`, approuvée par l'utilisateur 2026-10-07) ; plan `24_Plan_Decouplage_Sandbox.md`.
|
||||
- [x] **DC1** : Plugin LPAnimCore dans le jeu, LPAnimMeasure réduit à la mesure (DC1-T1..T5) (DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; tag addon lot-DC1-valide).
|
||||
- [ ] **DC2** : Commandlets de mods vers un module éditeur de la sandbox (DC2-T1..T5).
|
||||
- [x] **DC2** : Commandlets de mods vers un module éditeur de la sandbox (DC2-T1..T5) (DC2-VAL PASS ; tag addon lot-DC2-valide ; le jeu compile et démarre sans l'addon).
|
||||
- [ ] **DC3** : Scripts, données et sources de mods vers la sandbox (DC3-T1..T5).
|
||||
- [ ] **DC4** : Preuve « jeu seul », docs, évaluation (DC4-T1..T4).
|
||||
- [ ] Intégration des déplacements dans le jeu à améliorer (remarque utilisateur au contrôle visuel DC1, 2026-10-07) ; lot propriétaire : à définir, hors DC.
|
||||
- [x] Reports DC0 (lot propriétaire **DC1**, traité : texte corrigé en § Rupture du couplage) : § Rupture du couplage : la mention d'un appel à SetUALRoot par FLifePlayModule::StartupModule() est périmée ; seul l'addon l'appelle (LPSandbox.cpp:17, LPSandboxMeasure.cpp:39) — voir DC0-CART b2.
|
||||
- [ ] Report DC2 : `LPAnimCatalog` ignore `-DryRun` et a des valeurs par défaut (pack quaternius) : lancé sans arguments, il réécrit `Saved/AnimPipeline/quaternius/fiches_calculees.json` (DC2-VAL anomalie 1) — lot propriétaire DC3.
|
||||
- [ ] Report DC2 : `LifePlay.Build.cs` garde `Json` avec un commentaire AP1 périmé (plus aucun usage dans `LifePlay`) — lot propriétaire DC4.
|
||||
- [ ] Report DC2 : sans addon, l'absence est silencieuse au runtime ; l'éditeur/UBT affiche `AdditionalPluginDirectory … not found` (avertissement, non bloquant) — pour information, DC4 docs.
|
||||
## Lots (S0 … S8)
|
||||
|
||||
| Lot | Objet (une ligne) |
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,7 @@ 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-235 | 2026-10-07 | DC2 | jeu : `Source/LifePlayEditorTools/` (13 sources retirées : `LPAnimEvalCommandlet`, `LPAnimTwistCommandlet`, `LPAnimCatalogCommandlet`, `LPAnimIdentifyCommandlet`, `LPAnimChecks`, `LPHairFromStaticMeshCommandlet` en `.h/.cpp`, `LPAnimMethodSelect.h`), `LifePlayEditorTools.Build.cs`, docs (`21_Sandbox_Addon.md`, `20_Chaine_Animations.md`, `09_Commandes_Utiles.md`, `08_Guide_Developpeur.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC2/*`) ; addon `D:\Game\LifePlay-Sandbox` : module `LPModTools` (nouveau), `LPAnimMeasurePaths.h/.cpp` (nouveau), `LPSkeletonProfile.cpp`, `LPPoseMeasure.cpp`, `LPAnimMeasure.Build.cs`, `LPSandboxConfig.cpp`, `LPSandbox.uplugin` (commit `97c0d134cd1388c3735add79d77ea98817e89fbe`, branche `lot/DC2-modtools`, tag `lot-DC2-valide`) | lot DC2 : les commandlets de mods (`LPAnimEval`, `LPAnimTwist`, `LPAnimCatalog`, `LPAnimIdentify`, `LPHairFromStaticMesh`) passent dans le module éditeur `LPModTools` de l'addon ; le jeu compile sans l'addon (DC2-BUILD) et démarre sans lui (DC2-VAL E5) ; résultats identiques à SB2-REF (DC2-VAL PASS). Motif : le jeu public ne doit plus dépendre du dépôt privé `LifePlay-Sandbox` pour son outillage éditeur | pin `3e9c32f` → `97c0d13` (hash complet `97c0d134cd1388c3735add79d77ea98817e89fbe`) | `.\Tools\Restore.ps1 -Lot DC2` (jeu) et `.\Tools\Restore.ps1 -Lot DC2-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `3e9c32f` |
|
||||
| CHG-234 | 2026-10-07 | DC1 | jeu : `Plugins/LPAnimCore/*` (nouveau plugin : `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp`), `LifePlay.uproject`, `Source/LifePlay/LifePlay.Build.cs`, docs (`21_Sandbox_Addon.md`, `20_Chaine_Animations.md`, `07_Architecture_5.5.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC1/*`) ; addon `D:\Game\LifePlay-Sandbox` : `LPSandbox.uproject`, `LPSandbox.uplugin`, `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h`, `LPSandbox.Build.cs`, suppression de `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp` (commit `3e9c32f1f27b1abdaf7731dc90496c27ee061718`, branche `lot/DC1-animcore`, tag `lot-DC1-valide`) | lot DC1 : `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `LPAnimMeasure` réduit à la mesure ; compilation PASS, DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; report DC0 SetUALRoot traité. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (premier pas du découplage) | pin `16905c6` → `3e9c32f` (hash complet `3e9c32f1f27b1abdaf7731dc90496c27ee061718`) | `.\Tools\Restore.ps1 -Lot DC1` (jeu) et `.\Tools\Restore.ps1 -Lot DC1-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `16905c6` |
|
||||
| CHG-233 | 2026-10-07 | DC0 | `Docs/Migration/24_Plan_Decouplage_Sandbox.md` (nouveau), `Docs/Migration/etat/sandbox/DC0/DC0-CART.md`, `DC0-SPEC.md`, `DC0-VAL.md` (nouveaux) | lot DC0 : plan du découplage jeu / sandbox (lots DC0 à DC4), cartographie et SPEC approuvées par l'utilisateur le 2026-10-07 (plugin `LPAnimCore` dans le jeu, module éditeur `LPModTools` et tout l'outillage de mods dans l'addon, dépôt Sandbox privé) ; documentation seule, aucun code. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (`LifePlay` et `LifePlayEditorTools` dépendent de `LPAnimMeasure`) | aucun pin (documentation seule) | supprimer ces fichiers (documentation seule) ; rien n'est commité dans le dépôt du jeu ni poussé |
|
||||
| CHG-232 | 2026-10-07 | SB4 | addon `D:\Game\LifePlay-Sandbox` : `Plugins/LPSandbox/Source/LPSandbox/LPPairSandbox.cpp`, `LPSandboxModRegistry.cpp`, `LPSandboxModRegistry.h` (SB4-T0 : ouverture par déclencheur éphémère non mémorisée, `sandbox_mode.txt` du pilote), `README.md` (commits `16905c68070992f5139dab907b41b461fb765957` code + pin, tag `lot-SB4-valide`, branche `lot/SB4-final`, tag d'entrée `lot-SB4-avant` = `30e8c18`) ; jeu : `Docs/Migration/etat/sandbox/SB4/SB4-BUILD.md`, `SB4-VAL.md`, `SB4-EVAL.md`, `Docs/Migration/23_Cycle_De_Vie_Mod.md` (§ 7), `Docs/Migration/21_Sandbox_Addon.md`, `Docs/Migration/etat/sandbox/S3/S3-INT.md` | lot SB4 (dernier lot de la sandbox modulaire) : SB4-T0 correctif (ouverture par déclencheur éphémère, `Open=lps_couple` intact), compilation PASS, validation finale F1 à F4 PASS (démarrage, tournées Quaternius et FreeAnimationsPack identiques à SB2-REF, LPAnimEval identique), doc 23 § 7, évaluation `SB4-EVAL.md` : décision GO pour transposer au jeu par petits lots | pin `3d85852` → `16905c6` (hash complet `16905c68070992f5139dab907b41b461fb765957`) | `.\Tools\Restore.ps1 -Lot SB4` (docs du jeu), `.\Tools\Restore.ps1 -Lot SB4-addon -ProjectRoot D:\Game\LifePlay-Sandbox` et `git -C D:\Game\LifePlay-Sandbox checkout lot-SB4-avant` ; rien n'est commité dans le dépôt du jeu ni poussé |
|
||||
|
|
|
|||
148
Docs/Migration/etat/sandbox/DC2/DC2-BUILD.md
Normal file
148
Docs/Migration/etat/sandbox/DC2/DC2-BUILD.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# DC2-T2 : Compilation Addon – Rapport de Test
|
||||
|
||||
**Fiche :** DC2-T2 (Lot DC2, Agent A-BUILD)
|
||||
**Modèle prévu :** haiku (M0)
|
||||
**Modèle utilisé :** haiku (claude-haiku-4-5-20251001)
|
||||
**Date :** 2026-10-07
|
||||
**Responsable :** olivier.sylva@googlemail.com
|
||||
|
||||
## Objectif
|
||||
Compiler l'éditeur LifePlay deux fois :
|
||||
1. **AVEC** l'addon D:\Game\LifePlay-Sandbox en place
|
||||
2. **SANS** l'addon (dossier renommé temporairement)
|
||||
|
||||
Puis remettre l'addon en place. Aucune modification de code. Rapport factuel des résultats.
|
||||
|
||||
---
|
||||
|
||||
## Étapes Exécutées
|
||||
|
||||
### Étape 0 : Vérification des Processus
|
||||
**Résultat :** Aucun processus UnrealEditor actif. Compilation possible.
|
||||
|
||||
### Étape 1 : Compilation AVEC Addon
|
||||
**Commande :**
|
||||
`powershell
|
||||
& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex *> 'D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC2\DC2_Build_avec_addon.txt'
|
||||
`
|
||||
|
||||
**Code de sortie :** 0 (succès)
|
||||
|
||||
**Verdict :** ✓ Compilation réussie
|
||||
|
||||
**Erreurs :** 0 (aucune)
|
||||
|
||||
**Avertissements :** 5 warnings (standard)
|
||||
|
||||
**Modules compilés :**
|
||||
- LPAnimMeasure (ligne : "[35/94] Compile [x64] Module.LPAnimMeasure.cpp")
|
||||
- LPModTools (ligne : "[78/94] Compile [x64] LPModTools.cpp")
|
||||
- LPSandbox (ligne : "[91/94] Compile [x64] Module.LPSandbox.cpp")
|
||||
- LPAnimCore (pré-existant, non recompilé)
|
||||
- LifePlay (base du projet)
|
||||
- LifePlayEditorTools
|
||||
|
||||
**DLL générées :**
|
||||
- UnrealEditor-LPAnimMeasure.dll
|
||||
- UnrealEditor-LPModTools.dll
|
||||
- UnrealEditor-LPSandbox.dll
|
||||
|
||||
### Étape 2 : Renommage de l'Addon
|
||||
**Action :** Rename-Item 'D:\Game\LifePlay-Sandbox' 'LifePlay-Sandbox.__DC2_absent'
|
||||
|
||||
**Résultat :** ✓ Addon renommé avec succès
|
||||
|
||||
### Étape 3 : Compilation SANS Addon
|
||||
**Commande :** Identique à l'étape 1, but journal dans DC2_Build_sans_addon.txt
|
||||
|
||||
**Code de sortie :** 0 (succès)
|
||||
|
||||
**Verdict :** ✓ Compilation réussie
|
||||
|
||||
**Erreurs :** 0 (aucune)
|
||||
|
||||
**Avertissements :** 4 warnings (moins qu'avec addon, attendu)
|
||||
|
||||
**Message pertinent :**
|
||||
`
|
||||
AdditionalPluginDirectory D:\Game\LifePlay-Sandbox\Plugins not found. Path should be relative to the project
|
||||
`
|
||||
(Normal : l'addon est renommé, le chemin n'existe plus)
|
||||
|
||||
**Modules vérifiés :**
|
||||
- LPSandbox : ✓ NON compilé (addon absent)
|
||||
- LPAnimMeasure : ✓ NON compilé (dépend de l'addon)
|
||||
- LPModTools : ✓ NON compilé (dépend de l'addon)
|
||||
- LPAnimCore : ✓ Compilé ou à jour
|
||||
- LifePlay : ✓ Compilé
|
||||
- LifePlayEditorTools : ✓ Compilé
|
||||
|
||||
### Étape 4 : Remise en Place de l'Addon
|
||||
**Action (dans bloc finally) :** Rename-Item 'D:\Game\LifePlay-Sandbox.__DC2_absent' 'LifePlay-Sandbox'
|
||||
|
||||
**Résultat :** ✓ Addon remis en place
|
||||
|
||||
**Vérification :** Test-Path 'D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\LPSandbox.uplugin' → True
|
||||
|
||||
### Étape 5 : Analyse des Journaux
|
||||
Voir section Résumé ci-dessous.
|
||||
|
||||
### Étape 6 : Vérification des DLL et Dossiers
|
||||
|
||||
#### DLL Principales (UnrealEditor-*.dll)
|
||||
| Fichier | Date/Heure | Statut |
|
||||
|---------|-----------|--------|
|
||||
| UnrealEditor-LifePlay.dll | 07/10/2026 16:17:14 | Antérieur (non recompilé) |
|
||||
| UnrealEditor-LifePlayEditorTools.dll | 07/10/2026 18:34:49 | Recompilé (étape 1) |
|
||||
| UnrealEditor-LPAnimMeasure.dll | 07/10/2026 18:34:36 | Compilé (étape 1, avec addon) |
|
||||
| UnrealEditor-LPModTools.dll | 07/10/2026 18:34:50 | Compilé (étape 1, avec addon) |
|
||||
| UnrealEditor-LPSandbox.dll | 07/10/2026 18:34:55 | Compilé (étape 1, avec addon) |
|
||||
|
||||
#### DLL LPAnimCore Plugin
|
||||
| Fichier | Date/Heure |
|
||||
|---------|-----------|
|
||||
| UnrealEditor-LPAnimCore.dll | 07/10/2026 16:16:52 |
|
||||
|
||||
#### Dossier Binaries de l'Addon
|
||||
- **Path :** D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Binaries
|
||||
- **Existe :** ✓ Non (comportement attendu)
|
||||
|
||||
---
|
||||
|
||||
## Résumé des Résultats
|
||||
|
||||
| Aspect | AVEC Addon | SANS Addon |
|
||||
|--------|-----------|-----------|
|
||||
| **Code de sortie** | 0 | 0 |
|
||||
| **Erreurs** | 0 | 0 |
|
||||
| **Warnings** | 5 | 4 |
|
||||
| **LPSandbox compilé** | ✓ Oui | ✗ Non |
|
||||
| **LPAnimMeasure compilé** | ✓ Oui | ✗ Non |
|
||||
| **LPModTools compilé** | ✓ Oui | ✗ Non |
|
||||
| **LPAnimCore** | Compilé/à jour | Compilé/à jour |
|
||||
| **LifePlay** | Compilé | Compilé |
|
||||
| **LifePlayEditorTools** | Compilé | Compilé |
|
||||
| **Verdict** | ✓ Succès | ✓ Succès |
|
||||
|
||||
---
|
||||
|
||||
## Journaux
|
||||
- **Avec addon :** D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC2\DC2_Build_avec_addon.txt
|
||||
- **Sans addon :** D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC2\DC2_Build_sans_addon.txt
|
||||
|
||||
---
|
||||
|
||||
## Confirmation Finale
|
||||
- ✓ Addon remis en place : Test-Path 'D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\LPSandbox.uplugin' = **True**
|
||||
- ✓ Deux codes de sortie relevés : **0** (avec) et **0** (sans)
|
||||
- ✓ Aucun fichier source, script ou config modifié
|
||||
- ✓ Aucune commande git
|
||||
- ✓ Aucune suppression
|
||||
- ✓ Seuls fichiers créés : deux journaux et ce rapport
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
Les deux compilations réussissent sans erreur. L'addon AVEC produit les DLL attendues (LPSandbox, LPAnimMeasure, LPModTools). L'addon SANS exclut correctement ces modules tout en compilant LPAnimCore, LifePlay et LifePlayEditorTools. L'addon est remis en place et fonctionnel.
|
||||
|
||||
**Critère d'acceptation : VALIDÉ**
|
||||
29
Docs/Migration/etat/sandbox/DC2/DC2-DOC.md
Normal file
29
Docs/Migration/etat/sandbox/DC2/DC2-DOC.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# DC2-DOC — Pin et documentation du lot DC2
|
||||
|
||||
**Fiche DC2-T4 (A-INT, partie pin) et DC2-T5 (A-DOC) — Modèle prévu sonnet | utilisé sonnet**
|
||||
|
||||
- Date : 2026-10-07. Aucune commande git d'écriture, aucun fichier de code. Les fins de ligne (LF) des fichiers sont conservées.
|
||||
- Nouveau pin de l'addon : `97c0d134cd1388c3735add79d77ea98817e89fbe` (`97c0d13`, branche `lot/DC2-modtools`, tag `lot-DC2-valide`, local, non poussé). Pin précédent : `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (DC1).
|
||||
|
||||
## DC2-T4 (partie pin)
|
||||
|
||||
| Fichier | Lignes touchées |
|
||||
|---|---|
|
||||
| `Docs/Migration/etat/sandbox/S3/S3-INT.md` | 11 (commit pin), 13 (DC1 ajouté en tête des pins précédents), 49 (checkout) |
|
||||
| `Docs/Migration/21_Sandbox_Addon.md` | 127 (paragraphe Pin : nouveau hash en tête, DC1 en tête des précédents), 132 (checkout) |
|
||||
| `D:\Game\LifePlay-Sandbox\README.md` | 69 (commit pin), 70 (pin précédent), 75 (checkout) |
|
||||
|
||||
## DC2-T5 (documentation)
|
||||
|
||||
| Fichier | Lignes touchées |
|
||||
|---|---|
|
||||
| `Docs/Migration/Journal_Modifications.md` | 1134 : CHG-235 (7 colonnes), juste au-dessus de CHG-234 |
|
||||
| `Docs/Migration/21_Sandbox_Addon.md` | 87 (graphe : `LPModTools` → `LPAnimMeasure`, lien `LifePlayEditorTools` → `LPAnimMeasure` retiré) ; 107 (tableau des dépendances) ; 190 (case DC2 cochée) ; 195 à 197 (trois reports DC2 non cochés : `LPAnimCatalog` / `-DryRun` lot DC3, `Json` de `LifePlay.Build.cs` lot DC4, absence silencieuse de l'addon DC4) |
|
||||
| `Docs/Migration/20_Chaine_Animations.md` | 84 (commandlets dans `LPModTools`, chemins par `LPAnimMeasurePaths`) |
|
||||
| `Docs/Migration/09_Commandes_Utiles.md` | 61 (commentaire : `LPHairFromStaticMesh` exige l'addon), 102 (chaîne headless : module `LPModTools`, message d'erreur sans addon) |
|
||||
| `Docs/Migration/08_Guide_Developpeur.md` | 21 (phrase : l'addon n'est plus requis pour compiler ni lancer le jeu) |
|
||||
| `Docs/Migration/etat/sandbox/DC2/DC2-DOC.md` | nouveau |
|
||||
|
||||
## Contrôle
|
||||
|
||||
`git diff --stat` (lecture seule) : 08 = 1 ligne, 09 = 2 ajoutées / 1 retirée, 20 = 1 ligne, 21 = 8 ajoutées / 5 retirées, Journal = 1 ligne ajoutée, S3-INT = 3 lignes, README addon = 3 lignes. Aucun fichier réécrit en entier ; aucune fin de ligne CRLF introduite (tous les fichiers sont en LF).
|
||||
28
Docs/Migration/etat/sandbox/DC2/DC2-IMPL.md
Normal file
28
Docs/Migration/etat/sandbox/DC2/DC2-IMPL.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# DC2-IMPL — Commandlets de mods dans le module LPModTools de l'addon
|
||||
|
||||
- **Fiche** : DC2-T1 (A-IMPL-H) — **Modèle** : M3 opus | opus (session principale) — **Dépend de** : DC0-SPEC § 2 et § 3
|
||||
- **Instantanés** : `D:\Game\LifePlay_Backup_5.5\DC2` (14 fichiers du jeu) et `DC2-addon` (13 fichiers + `LPModTools` absent).
|
||||
Retour arrière proposé : `.\Tools\Restore.ps1 -Lot DC2` et `.\Tools\Restore.ps1 -Lot DC2-addon -ProjectRoot D:\Game\LifePlay-Sandbox`.
|
||||
- **Branche addon** : `lot/DC2-modtools` (depuis `lot/DC1-animcore` = `0aca51d`).
|
||||
|
||||
## Addon
|
||||
|
||||
| Fichier | Changement |
|
||||
|---|---|
|
||||
| `Plugins/LPSandbox/Source/LPModTools/` | nouveau module **Editor** : `LPModTools.Build.cs` (`bUseUnity=false`, plat ; public `Core`, `CoreUObject`, `Engine`, `LPAnimMeasure` ; privé `UnrealEd`, `AssetRegistry`, `Json`, `MeshDescription`, `StaticMeshDescription`, `SkeletalMeshUtilitiesCommon`, `SkeletalMeshDescription`, `AnimationCore`), `LPModTools.cpp` |
|
||||
| idem | 13 sources copiées du jeu, **SHA-256 identiques** : `LPAnimEvalCommandlet`, `LPAnimTwistCommandlet`, `LPAnimCatalogCommandlet`, `LPAnimIdentifyCommandlet`, `LPAnimChecks`, `LPHairFromStaticMeshCommandlet` (`.h/.cpp`), `LPAnimMethodSelect.h` ; aucune macro d'export à renommer (aucune n'utilisait `LIFEPLAYEDITORTOOLS_API`) ; puis une seule modification : `LPAnimIdentifyCommandlet.cpp` lit les profils par `LPAnimMeasurePaths::ToolsAnimDir()` |
|
||||
| `LPSandbox.uplugin` | module `LPModTools` (Editor, Default), description |
|
||||
| `LPAnimMeasure/LPAnimMeasurePaths.h/.cpp` | nouveau : `SandboxRoot()` (base du plugin `LPSandbox` + `../..`, journal `PATHS racine addon …`), `ToolsAnimDir()` (`<addon>/Tools/anim` s'il existe, sinon `<projet>/Tools/anim` jusqu'à DC3) |
|
||||
| `LPSkeletonProfile.cpp`, `LPPoseMeasure.cpp` | `ProjectDir()/Tools/anim/{profiles,methodes}` → `ToolsAnimDir()/…` |
|
||||
| `LPAnimMeasure.Build.cs` | dépendance privée `Projects` |
|
||||
| `LPSandbox/LPSandboxConfig.cpp` | lit `<addon>/Config/LPSandbox.ini` (identique à la copie du jeu), repli `ProjectConfigDir()` jusqu'à DC3 |
|
||||
|
||||
## Jeu
|
||||
|
||||
| Fichier | Changement |
|
||||
|---|---|
|
||||
| `Source/LifePlayEditorTools/` | 13 sources retirées (`git rm`) |
|
||||
| `LifePlayEditorTools.Build.cs` | `LPAnimMeasure` retiré ; retirés aussi `StaticMeshDescription`, `SkeletalMeshUtilitiesCommon`, `SkeletalMeshDescription`, `AnimationCore` et le doublon `Json` (seuls les fichiers déplacés les utilisaient ; `MeshDescription` reste pour `LPCheckMeshes`, `Json` pour `LPEditWidget`) |
|
||||
|
||||
Après DC2, plus aucun `Build.cs` ni `.uproject` du jeu ne cite `LPAnimMeasure`. Note : `LifePlay.Build.cs` garde
|
||||
`Json` avec un commentaire AP1 périmé (aucun `.cpp` de `LifePlay` n'utilise Json) : laissé tel quel, reporté (DC4, nettoyage).
|
||||
74
Docs/Migration/etat/sandbox/DC2/DC2-VAL.md
Normal file
74
Docs/Migration/etat/sandbox/DC2/DC2-VAL.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# DC2-VAL — Validation automatique du lot DC2 (DC2-T3)
|
||||
|
||||
**Fiche DC2-T3 — Modèle prévu sonnet | utilisé sonnet**
|
||||
|
||||
- Date : 2026-10-07. Build testé : celui de `DC2-BUILD.md` (code 0, non recompilé). Lot DC2 : commandlets de mods (LPAnimEval, LPAnimTwist, LPAnimCatalog, LPAnimIdentify, LPHairFromStaticMesh) déplacés dans le module éditeur `LPModTools` de l'addon.
|
||||
- Journaux copiés dans `D:\Game\LifePlay_Backup_5.5\DC2-VAL\` : `E1_*_console.txt`, `E2_*_console.txt`, `E2_*_result.txt`, `E2_*_sandbox.log`, `E3_console.txt` + `E3_out\`, `DC2_LPAnimTwist_*.log/.txt`, `E4_*_console.txt`, `DC2_LPAnimCatalog_*.log`, `DC2_LPHairFromStaticMesh_*.log`, `E5a_console.txt`, `DC2_LPAnimEval_20261007-190750.log`, `E5b_game_noaddon.log`, `AP7_LPAnim*.log` (le dossier contient aussi d'anciens `AP7_*` copiés par un joker trop large ; seuls les `AP7_LPAnimIdentify_20261007-183742/183801` et `AP7_LPAnimEval_20261007-183828/185814` sont de cette validation).
|
||||
- Méthode : celle de `DC1-VAL.md` / `SB4-VAL.md`, tournées par `AnimPipeline.ps1` (pwsh 7), aucun éditeur avant chaque lancement. Aucune commande git hors `status`, aucun source/script/ini modifié, aucune recompilation.
|
||||
|
||||
## Tableau des essais
|
||||
|
||||
| Essai | Attendu | Constaté (lignes recopiées) | Verdict |
|
||||
|---|---|---|---|
|
||||
| E1 quaternius `-From 2 -To 2` | code 0, verdict ok, fichiers = réf | code 0, 14 s. `[ok] identification.json valide : profil ue4_mannequin, methode ue4`, `verdict : ok (14 s)`. Journal : `LogLPAnimMeasure: PATHS racine addon D:/Game/LifePlay-Sandbox`. `identification.json` SHA-256 identique à SB2-REF (310CCE61A3B7). `etapes\2_identification.json` : SHA différent (0AF3F335AAB3 réf / BF81B7520808 nouveau), JSON égal une fois `debut`/`fin`/`duree_s` ignorés. | PASS |
|
||||
| E1 freeanims | idem | code 0, 13 s. `profil ue5_mannequin, methode ue5`, `verdict : ok`. `identification.json` identique à la réf (557013ADCB21). `etapes\2_identification.json` : SHA différent (E282521BBA35 / 345D5EBB5A56), JSON égal hors dates. | PASS |
|
||||
| E2 quaternius `-From 5 -To 5` | code 0, `avertissement`, 8 avertissements, 44 fichiers | code 0, 1136 s, `verdict : avertissement (1136 s)`. Journal éditeur : `LogLPAnimMeasure: PATHS racine addon D:/Game/LifePlay-Sandbox` ; `TOURNEE debut 43 clips, 129 etapes` ; `Ouvert : lps_asset_integration (declencheur, non memorise)` ; `TOURNEE debut 43 clips, 2080 etapes` ; `TOURNEE fermeture demandee`. `Open=lps_couple` avant et après. Comparaison : voir ci-dessous. | PASS |
|
||||
| E2 freeanims | code 0, `ok`, 0 avertissement, 73 fichiers | code 0, 435 s, `verdict : ok (435 s)`. Journal : `PATHS racine addon D:/Game/LifePlay-Sandbox` ; `TOURNEE debut 10 clips, 30 etapes` ; `Ouvert : lps_asset_integration (declencheur, non memorise)` ; `TOURNEE debut 10 clips, 697 etapes` ; `TOURNEE fermeture demandee`. `Open=lps_couple` après. | PASS |
|
||||
| E3 LPAnimTwist à blanc | code 0, rapport valide | Arguments reconstitués avec certitude depuis les commandes journalisées par le pilote (`etapes\5_evaluation.json`, ligne LPAnimTwist) : `-Methode=ue4 -ClipDir=/Game/Quaternius/UAL -RefClip=A_TPose -BodyClipDir=/Game/Quaternius/UAL -Templates=Bridget:/Game/Heavy/Daz/Anims/Idle_F_Daz,Michael:/Game/Heavy/Daz/Anims/idle_Daz -Bodies=Bridget:B_,Michael:M_ -Out=...` + `-DryRun`. **Écart volontaire** : `-Out` pointé vers `D:/Game/LifePlay_Backup_5.5/DC2-VAL/E3_out` (le commandlet écrit `rapport_torsion.json` même à blanc ; cela aurait écrasé un fichier de résultat comparé à la réf). Code 0, 23 s. `LogLPAnimTwist: Display: TORSION Bridget profil genesis8 : 6 paires Bend / Twist` ; lignes `TORSION B_Walk_Loop 41 cles, torsion deplacee 0.0 deg, conservation 0.00000 deg 0.00000 cm (essai)` ; `TORSION resultat VALIDE : 0 echec(s)` ; `rapport_torsion.json` : `essai_a_blanc=true`, `valide=true`. Journal : `PATHS racine addon D:/Game/LifePlay-Sandbox`. `Saved\AnimPipeline\quaternius\rapport_torsion.json` inchangé (SHA D7A3BF92E174 = réf). | PASS |
|
||||
| E4 LPAnimCatalog (`-DryRun` seul) | trouvé (pas « introuvable ») | `LogInit: Executing Class /Script/LPModTools.LPAnimCatalogCommandlet`. Code 0 : **pas d'erreur d'argument** — le commandlet a des valeurs par défaut (pack quaternius) et a écrit `LogLPAnimCatalog: Warning: CATALOGUE fiches 42 echecs 0 -> .../Saved/AnimPipeline/quaternius/fiches_calculees.json` (voir anomalie 1). Aucun asset UE écrit. | PASS (trouvé) — écriture signalée |
|
||||
| E4 LPHairFromStaticMesh (`-DryRun` seul) | trouvé, erreur d'argument | `LogInit: Executing Class /Script/LPModTools.LPHairFromStaticMeshCommandlet` puis `LogLPHairFromStatic: Error: Usage : -Static=<StaticMesh> -FitHead=<StaticMesh> -Out=</Game/...> [-Body=] [-RefHair=] [-Bone=head] [-Scale=] [-OffsetZ=] [-OffsetY=] [-DryRun]`, `result 1`. Aucune écriture. | PASS |
|
||||
| E5a jeu sans addon, `LPAnimEval` | commandlet introuvable | Code 1 : `LogInit: Error: LPAnimEvalCommandlet looked like a commandlet, but we could not find the class.` | PASS |
|
||||
| E5b jeu `-game` sans addon (120 s) | LPAnimCore monté, LPSandbox absent, carte chargée | `LogPluginManager: Mounting Project plugin McpAutomationBridge` / `LPAnimCore` / `ModdingExtra` / `StreetMap` (aucune ligne LPSandbox ; aucune ligne `AdditionalPluginDirectory` ni refus d'aucune sorte dans ce journal ; le projet reste lancé sans message d'erreur, `LifePlay.uproject` référence toujours le plugin LPSandbox et le répertoire `../LifePlay-Sandbox/Plugins`). `LogLoad: LoadMap: /Game/Scenes/CentralLondon?Name=Player` puis `Took 0.790986 seconds to LoadMap(/Game/Scenes/CentralLondon)`. Seul `Error` : `LogPlayerController: Error: InputMode:UIOnly - Attempting to focus Non-Focusable widget SObjectWidget [Widget.cpp(968)]!` (UI du menu, voir anomalie 3). Aucun Fatal/Ensure. Pas de ligne `Coiffures : ... introuvable` (l'Error Coiffures connu est propre à la sandbox ; ici : `Coiffures : base 126 entree(s) ... table de 126 entree(s), 89 morph(s)`). Processus arrêté (`Stop-Process`). | PASS |
|
||||
| E6 état final | ini standard, 0 éditeur, addon en place | `Saved\sandbox_mods.ini` : `Open=lps_couple` (3 modules activés, ordre standard) ; 0 `UnrealEditor` ; `Test-Path D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\LPSandbox.uplugin` = True ; `LifePlay-Sandbox.__DC2_absent` n'existe plus. | PASS |
|
||||
|
||||
## Détail E2 : comparaison complète contre `SB2-REF\<pack>\`
|
||||
|
||||
Règle DC1-VAL/SB4-VAL : SHA-256 de chaque fichier commun ; exclus : `etapes\5_evaluation.json` (JSON sur verdict + avertissements), `etat.json`, ligne « Généré le » de `rapport.html`, images freeanims (les 20 connues). Le dossier `precedent_*` est inclus dans le décompte (comme aux lots précédents).
|
||||
|
||||
### quaternius : 44 fichiers comparés (liste 44 / 44 identique)
|
||||
- 40 fichiers au SHA-256 identique. Seuls différents : `etat.json` (`fin`/`maj` uniquement), `rapport.html` (seule ligne : « Généré le » 07/10/2026 12:10:18 → 18:38:49), `etapes\5_evaluation.json` (verdict `avertissement` des deux côtés ; 8 avertissements, listes égales en JSON ; durée 1136 s = 1136 s) et `etapes\2_identification.json` (réécrit par E1, dates seules).
|
||||
- Tous les `.json`/`.csv`/`.txt` de données (`rapport.json`, `eval_*.csv`, `controles_*.csv`, `retarget_pose_*.txt`, `rapport_torsion.json`, `fiches_calculees.json`...) identiques.
|
||||
|
||||
### freeanims : 73 fichiers comparés (liste 73 / 73 identique)
|
||||
- 49 fichiers identiques. 24 différents : les 4 fichiers exclus ci-dessus (`etat.json`, `rapport.html` « Généré le » 12:31:18 → 18:58:36, `etapes\5_evaluation.json` : verdict `ok`, 0 avertissement des deux côtés, durée 435 s = 435 s, `etapes\2_identification.json` dates) et **20 images**, exactement les 20 connues (10 planches `.jpg` + 10 vignettes `.png` : AirKiss, AttackInLevitation2, ChillDriving, ComboHands, EatingOnParty, FlyingUp, GoForward, Rest, RightLegKick2, Talk), écarts de taille inférieurs à 0,3 % (ex. AS_AirKiss.png 261470 → 261276 ; AS_Talk.png 260083 → 259267).
|
||||
- 73 − 4 − 20 = 49 fichiers de données identiques.
|
||||
|
||||
## Anomalies
|
||||
|
||||
1. **E4 LPAnimCatalog sans argument n'échoue pas** : le commandlet a des valeurs par défaut (pack quaternius) et, même avec `-DryRun` (qui ne concerne pas ce commandlet), il a réécrit `Saved\AnimPipeline\quaternius\fiches_calculees.json` (fichier de résultat, pas un asset UE). Seule différence avec la réf : chemin `livraison` en absolu au lieu de relatif (`D:/Game/...` vs `../../../../../../LifePlay-master 5.5/...`). J'ai restauré le fichier par copie depuis `SB2-REF\quaternius\fiches_calculees.json` (il était identique à la réf juste avant E4, cf. E2) ; SHA vérifié après copie (5DBBA632FAF3). Comportement hérité du commandlet d'origine, non lié au déplacement (le code est identique octet pour octet).
|
||||
2. E3 : `-Out` redirigé hors de `Saved\AnimPipeline` (voir tableau) ; les 204 avertissements du résumé RunCommandlet sont les lignes `TORSION` journalisées en Warning/Display, pas des erreurs.
|
||||
3. E5b : `LogPlayerController: Error: InputMode:UIOnly...` : non rencontré dans les journaux de la sandbox (autre scène), vraisemblablement propre au menu du jeu ; non analysé plus avant (hors périmètre DC2). Pas de ligne `AdditionalPluginDirectory ... not found` dans le journal `-game` (contrairement au journal de build DC2-BUILD) : l'absence de l'addon est silencieuse au runtime.
|
||||
4. E5a et E5b ont été lancés dans deux séquences de renommage distinctes (l'addon avait été remis en place entre les deux par le `finally`, puis renommé de nouveau pour E5b) ; vérification `Test-Path ...LPSandbox.uplugin` = True après chacune.
|
||||
5. Les chemins `Tools/anim` : l'addon n'a pas de `Tools\anim` (Test-Path = False) ; les commandlets (Identify lit `profiles`, Eval/Twist lisent `methodes`) ont fonctionné en repli sur le jeu. Le journal ne trace que `PATHS racine addon D:/Game/LifePlay-Sandbox`, pas le dossier `Tools/anim` retenu ; le repli est prouvé indirectement par le succès de E1/E2/E3 et l'identité des résultats.
|
||||
6. Éditeur plantant à la fermeture : non observé.
|
||||
|
||||
## État final (E6)
|
||||
|
||||
- `Saved\sandbox_mods.ini` : `[Mods] Order=lps_couple,lps_interaction,lps_asset_integration`, `Open=lps_couple`, `[Enabled]` les trois à `true`.
|
||||
- `UnrealEditor` : 0 processus.
|
||||
- `git -C D:\Game\LifePlay-Sandbox status --short` (changements DC2 non commités, attendu) :
|
||||
```
|
||||
M Plugins/LPSandbox/LPSandbox.uplugin
|
||||
M Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasure.Build.cs
|
||||
M Plugins/LPSandbox/Source/LPAnimMeasure/LPPoseMeasure.cpp
|
||||
M Plugins/LPSandbox/Source/LPAnimMeasure/LPSkeletonProfile.cpp
|
||||
M Plugins/LPSandbox/Source/LPSandbox/LPSandboxConfig.cpp
|
||||
?? Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasurePaths.cpp
|
||||
?? Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasurePaths.h
|
||||
?? Plugins/LPSandbox/Source/LPModTools/
|
||||
```
|
||||
- Addon en place : `LifePlay-Sandbox.__DC2_absent` absent, `LPSandbox.uplugin` présent.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** (partie automatique) : E1 (identification identique à la réf), E2 (quaternius `avertissement` 8 avertissements, 44 fichiers ; freeanims `ok`, 73 fichiers ; données identiques, seules différences dates, « Généré le » et les 20 images non déterministes ; `Open=lps_couple` préservé), E3 (LPAnimTwist valide à blanc), E4 (les deux commandlets sont trouvés dans `/Script/LPModTools`), E5 ((a) introuvable sans addon ; (b) LPAnimCore monté, LPSandbox absent, carte CentralLondon chargée), E6 (addon en place). Le nouveau code tourne (`PATHS racine addon` dans tous les journaux d'éditeur testés).
|
||||
|
||||
Point à noter pour l'utilisateur : anomalie 1 (LPAnimCatalog écrit `fiches_calculees.json` sans argument, fichier restauré).
|
||||
|
||||
## Modèles par fiche
|
||||
|
||||
| Fiche | Modèle prévu | Modèle utilisé |
|
||||
|---|---|---|
|
||||
| DC2-T1 | opus | opus |
|
||||
| DC2-T2 | haiku | haiku |
|
||||
| DC2-T3 | sonnet | sonnet |
|
||||
|
|
@ -8,9 +8,9 @@ Date : 2026-10-06 — Role A-INT
|
|||
|---|---|
|
||||
| Repo addon | https://github.com/edujiro5972/LifePlay-Sandbox.git |
|
||||
| Branche | `main` |
|
||||
| **Commit pin** | `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (`3e9c32f`, DC1 : `LPAnimRedirect` et `LPCoupleSim` rapatries dans le plugin `LPAnimCore` du jeu, branche `lot/DC1-animcore`, tag `lot-DC1-valide`, **local, non pousse** au 2026-10-07) |
|
||||
| **Commit pin** | `97c0d134cd1388c3735add79d77ea98817e89fbe` (`97c0d13`, DC2 : module editeur `LPModTools` et chemins propres a l'addon, branche `lot/DC2-modtools`, tag `lot-DC2-valide`, **local, non pousse** au 2026-10-07) |
|
||||
| Contenu du commit | S5-T2 : `LPCoupleSim` (géométrie du couple partagée jeu/addon) ; inclut S4-T3 (retrait GM Interaction legacy), S4-T2 (découpe `LPPairSandbox.cpp`) et S3-T4 (`StartupModule` → `SetUALRoot`) |
|
||||
| Pins précédents | `16905c68070992f5139dab907b41b461fb765957` (SB4), `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
|
||||
| Pins précédents | `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (DC1), `16905c68070992f5139dab907b41b461fb765957` (SB4), `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
|
||||
|
||||
## Structure
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ sur la copie effectivement chargee. Pin manuel a la place (ci-dessous).
|
|||
```powershell
|
||||
cd D:\Game
|
||||
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
|
||||
git -C LifePlay-Sandbox checkout 3e9c32f1f27b1abdaf7731dc90496c27ee061718
|
||||
git -C LifePlay-Sandbox checkout 97c0d134cd1388c3735add79d77ea98817e89fbe
|
||||
# Le jeu doit etre clone a cote : D:\Game\LifePlay-master 5.5
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,348 +0,0 @@
|
|||
// Lot AP6 - voir LPAnimCatalogCommandlet.h
|
||||
|
||||
#include "LPAnimCatalogCommandlet.h"
|
||||
|
||||
#include "Animation/AnimData/IAnimationDataModel.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Dom/JsonObject.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(LogLPAnimCatalog, Log, All);
|
||||
|
||||
namespace LPAnimCatalogImpl
|
||||
{
|
||||
// Seuils de classement (definitions : voir l'en-tete).
|
||||
constexpr float LyingTilt = 60.f; // degres
|
||||
constexpr float KneelHeight = 12.f; // cm
|
||||
constexpr float LowHips = 0.72f; // fraction de la hauteur de hanche debout (gardes de combat : debout)
|
||||
constexpr float SeatBehind = 20.f; // cm
|
||||
constexpr float Majority = 0.6f;
|
||||
constexpr float FootContact = 3.f; // cm
|
||||
constexpr float HandRest = 15.f; // cm/s
|
||||
constexpr float HandRaised = 0.3f;
|
||||
|
||||
int32 BoneOf(const FLPPose& Pose, const TCHAR* Role, int32 Index = 0)
|
||||
{
|
||||
const FName Name = LPPoseMeasure::RoleBone(Pose, Role, Index);
|
||||
return Name.IsNone() ? INDEX_NONE : Pose.Names.IndexOfByKey(Name);
|
||||
}
|
||||
|
||||
FVector At(const FLPPose& Pose, int32 Bone)
|
||||
{
|
||||
return Pose.Component.IsValidIndex(Bone) ? Pose.Component[Bone].GetLocation() : FVector::ZeroVector;
|
||||
}
|
||||
|
||||
FVector RestAt(const FLPPose& Pose, int32 Bone)
|
||||
{
|
||||
return Pose.Rest.IsValidIndex(Bone) ? Pose.Rest[Bone].GetLocation() : FVector::ZeroVector;
|
||||
}
|
||||
|
||||
FVector Flat(FVector V)
|
||||
{
|
||||
V.Z = 0.f;
|
||||
return V;
|
||||
}
|
||||
|
||||
struct FBones
|
||||
{
|
||||
int32 Hips, Head, ThighL, ThighR, CalfL, CalfR, FootL, FootR, ToeL, ToeR, HandL, HandR;
|
||||
bool Bind(const FLPPose& P)
|
||||
{
|
||||
Hips = BoneOf(P, TEXT("hips")); Head = BoneOf(P, TEXT("head"));
|
||||
ThighL = BoneOf(P, TEXT("thigh_l")); ThighR = BoneOf(P, TEXT("thigh_r"));
|
||||
CalfL = BoneOf(P, TEXT("calf_l")); CalfR = BoneOf(P, TEXT("calf_r"));
|
||||
FootL = BoneOf(P, TEXT("foot_l")); FootR = BoneOf(P, TEXT("foot_r"));
|
||||
ToeL = BoneOf(P, TEXT("toe_l")); ToeR = BoneOf(P, TEXT("toe_r"));
|
||||
HandL = BoneOf(P, TEXT("hand_l")); HandR = BoneOf(P, TEXT("hand_r"));
|
||||
const int32 All[] = { Hips, Head, ThighL, ThighR, CalfL, CalfR, FootL, FootR, ToeL, ToeR, HandL, HandR };
|
||||
for (const int32 B : All)
|
||||
{
|
||||
if (B == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
FVector ForwardOf(const FLPPose& P, const FBones& B)
|
||||
{
|
||||
// Haut x gauche = avant (verifie sur Roll, qui avance, et Sitting_Enter, dont le bassin recule).
|
||||
const FVector Side = Flat(At(P, B.ThighL) - At(P, B.ThighR));
|
||||
return FVector::CrossProduct(FVector::UpVector, Side.GetSafeNormal()).GetSafeNormal();
|
||||
}
|
||||
|
||||
const TCHAR* PostureOf(const FLPPose& P, const FBones& B, float StandingHips, float Floor)
|
||||
{
|
||||
const FVector Trunk = At(P, B.Head) - At(P, B.Hips);
|
||||
const float Tilt = static_cast<float>(FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Trunk.GetSafeNormal().Z, -1.0, 1.0))));
|
||||
if (Tilt > LyingTilt)
|
||||
{
|
||||
return TEXT("allonge");
|
||||
}
|
||||
const float Hips = static_cast<float>(At(P, B.Hips).Z) - Floor;
|
||||
if (Hips >= LowHips * StandingHips)
|
||||
{
|
||||
return TEXT("debout");
|
||||
}
|
||||
const float Knee = static_cast<float>(FMath::Min(At(P, B.CalfL).Z, At(P, B.CalfR).Z)) - Floor;
|
||||
if (Knee < KneelHeight)
|
||||
{
|
||||
return TEXT("a genoux");
|
||||
}
|
||||
const FVector Ankles = (At(P, B.FootL) + At(P, B.FootR)) * 0.5;
|
||||
const float Behind = static_cast<float>(FVector::DotProduct(Flat(Ankles - At(P, B.Hips)), ForwardOf(P, B)));
|
||||
return Behind > SeatBehind ? TEXT("assis") : TEXT("accroupi");
|
||||
}
|
||||
|
||||
bool ReadJson(const FString& Path, TSharedPtr<FJsonValue>& Out)
|
||||
{
|
||||
FString Text;
|
||||
if (!FFileHelper::LoadFileToString(Text, *Path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Text);
|
||||
return FJsonSerializer::Deserialize(Reader, Out) && Out.IsValid();
|
||||
}
|
||||
|
||||
struct FClipInfo
|
||||
{
|
||||
int32 Frames = 0;
|
||||
double Rate = 0.0;
|
||||
float Duration = 0.f;
|
||||
};
|
||||
|
||||
bool ClipInfo(const FString& Path, FClipInfo& Out)
|
||||
{
|
||||
const UAnimSequence* Anim = LoadObject<UAnimSequence>(nullptr, *Path);
|
||||
const IAnimationDataModel* Model = Anim ? Anim->GetDataModel() : nullptr;
|
||||
if (!Model)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Out.Frames = Model->GetNumberOfKeys();
|
||||
Out.Rate = Model->GetFrameRate().AsDecimal();
|
||||
Out.Duration = static_cast<float>(Model->GetPlayLength());
|
||||
return Out.Frames > 0;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Measure(const FString& Path, const FClipInfo& Info, FString& Error)
|
||||
{
|
||||
TSharedRef<FJsonObject> Card = MakeShared<FJsonObject>();
|
||||
TArray<FLPPose> Poses;
|
||||
Poses.SetNum(Info.Frames);
|
||||
for (int32 Key = 0; Key < Info.Frames; ++Key)
|
||||
{
|
||||
const float Time = FMath::Min(static_cast<float>(Key / Info.Rate), Info.Duration);
|
||||
if (!LPPoseMeasure::Sample(Path, Time, Poses[Key]))
|
||||
{
|
||||
Error = FString::Printf(TEXT("echantillon impossible a %.3f s"), Time);
|
||||
return Card;
|
||||
}
|
||||
}
|
||||
FBones B;
|
||||
if (!B.Bind(Poses[0]))
|
||||
{
|
||||
Error = FString::Printf(TEXT("roles d'os manquants (profil %s)"), *Poses[0].Profile);
|
||||
return Card;
|
||||
}
|
||||
const FLPPose& First = Poses[0];
|
||||
const FLPPose& Last = Poses.Last();
|
||||
const float Floor = static_cast<float>(FMath::Min(RestAt(First, B.FootL).Z, RestAt(First, B.FootR).Z));
|
||||
const float StandingHips = static_cast<float>(RestAt(First, B.Hips).Z) - Floor;
|
||||
const float Height = FMath::Max(1.f, static_cast<float>(RestAt(First, B.Head).Z) - Floor);
|
||||
|
||||
Card->SetNumberField(TEXT("images"), Info.Frames);
|
||||
Card->SetNumberField(TEXT("cadence"), FMath::RoundToFloat(static_cast<float>(Info.Rate) * 100.f) / 100.f);
|
||||
Card->SetNumberField(TEXT("duree_s"), FMath::RoundToFloat(Info.Duration * 100.f) / 100.f);
|
||||
|
||||
// Boucle : extremites relatives a la hanche, premiere et derniere image.
|
||||
const int32 Ends[] = { B.HandL, B.HandR, B.FootL, B.FootR, B.Head };
|
||||
float LoopGap = 0.f;
|
||||
for (const int32 E : Ends)
|
||||
{
|
||||
const FVector A = At(First, E) - At(First, B.Hips);
|
||||
const FVector Z = At(Last, E) - At(Last, B.Hips);
|
||||
LoopGap = FMath::Max(LoopGap, static_cast<float>((A - Z).Size()));
|
||||
}
|
||||
const float LoopPct = 100.f * LoopGap / Height;
|
||||
Card->SetNumberField(TEXT("boucle_ecart_pct"), FMath::RoundToFloat(LoopPct * 100.f) / 100.f);
|
||||
|
||||
// Deplacement horizontal de la hanche.
|
||||
const FVector Travel = Flat(At(Last, B.Hips) - At(First, B.Hips));
|
||||
const float Distance = static_cast<float>(Travel.Size());
|
||||
Card->SetNumberField(TEXT("deplacement_cm"), FMath::RoundToFloat(Distance));
|
||||
Card->SetNumberField(TEXT("vitesse_cm_s"), Info.Duration > 0.f ? FMath::RoundToFloat(Distance / Info.Duration) : 0.f);
|
||||
const float Ahead = Distance > 0.f ? static_cast<float>(FVector::DotProduct(Travel / Distance, ForwardOf(First, B))) : 0.f;
|
||||
Card->SetStringField(TEXT("deplacement_sens"), Ahead > 0.7f ? TEXT("avant") : Ahead < -0.7f ? TEXT("arriere") : TEXT("cote"));
|
||||
|
||||
// Posture image par image.
|
||||
TMap<FString, int32> Count;
|
||||
TArray<float> SeatHeights;
|
||||
int32 FeetDown[2] = { 0, 0 };
|
||||
int32 Raised[2] = { 0, 0 };
|
||||
float HandPath[2] = { 0.f, 0.f };
|
||||
const float RestFoot[2] = { static_cast<float>(RestAt(First, B.FootL).Z), static_cast<float>(RestAt(First, B.FootR).Z) };
|
||||
const float RestToe[2] = { static_cast<float>(RestAt(First, B.ToeL).Z), static_cast<float>(RestAt(First, B.ToeR).Z) };
|
||||
for (int32 Key = 0; Key < Info.Frames; ++Key)
|
||||
{
|
||||
const FLPPose& P = Poses[Key];
|
||||
const FString Posture = PostureOf(P, B, StandingHips, Floor);
|
||||
++Count.FindOrAdd(Posture);
|
||||
if (Posture == TEXT("assis"))
|
||||
{
|
||||
SeatHeights.Add(static_cast<float>((At(P, B.ThighL).Z + At(P, B.ThighR).Z) * 0.5) - Floor);
|
||||
}
|
||||
const int32 Feet[2] = { B.FootL, B.FootR };
|
||||
const int32 Toes[2] = { B.ToeL, B.ToeR };
|
||||
const int32 Hands[2] = { B.HandL, B.HandR };
|
||||
for (int32 Side = 0; Side < 2; ++Side)
|
||||
{
|
||||
const float Lift = FMath::Min(static_cast<float>(At(P, Feet[Side]).Z) - RestFoot[Side],
|
||||
static_cast<float>(At(P, Toes[Side]).Z) - RestToe[Side]);
|
||||
FeetDown[Side] += Lift < FootContact ? 1 : 0;
|
||||
Raised[Side] += At(P, Hands[Side]).Z > At(P, B.Head).Z ? 1 : 0;
|
||||
if (Key > 0)
|
||||
{
|
||||
const FLPPose& Q = Poses[Key - 1];
|
||||
HandPath[Side] += static_cast<float>(((At(P, Hands[Side]) - At(P, B.Hips)) - (At(Q, Hands[Side]) - At(Q, B.Hips))).Size());
|
||||
}
|
||||
}
|
||||
}
|
||||
FString Main = TEXT("variable");
|
||||
for (const TPair<FString, int32>& It : Count)
|
||||
{
|
||||
if (It.Value >= Majority * Info.Frames)
|
||||
{
|
||||
Main = It.Key;
|
||||
}
|
||||
}
|
||||
Card->SetStringField(TEXT("posture"), Main);
|
||||
Card->SetStringField(TEXT("posture_debut"), PostureOf(First, B, StandingHips, Floor));
|
||||
Card->SetStringField(TEXT("posture_fin"), PostureOf(Last, B, StandingHips, Floor));
|
||||
TSharedRef<FJsonObject> Split = MakeShared<FJsonObject>();
|
||||
for (const TPair<FString, int32>& It : Count)
|
||||
{
|
||||
Split->SetNumberField(It.Key, FMath::RoundToFloat(100.f * It.Value / Info.Frames));
|
||||
}
|
||||
Card->SetObjectField(TEXT("posture_pct"), Split);
|
||||
if (SeatHeights.Num() > 0)
|
||||
{
|
||||
SeatHeights.Sort();
|
||||
Card->SetNumberField(TEXT("hauteur_assise_cm"), FMath::RoundToFloat(SeatHeights[SeatHeights.Num() / 2]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Card->SetField(TEXT("hauteur_assise_cm"), MakeShared<FJsonValueNull>());
|
||||
}
|
||||
const TCHAR* SideNames[2] = { TEXT("gauche"), TEXT("droite") };
|
||||
TSharedRef<FJsonObject> Feet = MakeShared<FJsonObject>();
|
||||
TSharedRef<FJsonObject> Hands = MakeShared<FJsonObject>();
|
||||
for (int32 Side = 0; Side < 2; ++Side)
|
||||
{
|
||||
Feet->SetNumberField(SideNames[Side], FMath::RoundToFloat(100.f * FeetDown[Side] / Info.Frames));
|
||||
const float Speed = Info.Duration > 0.f ? HandPath[Side] / Info.Duration : 0.f;
|
||||
const TCHAR* State = Raised[Side] >= HandRaised * Info.Frames ? TEXT("levee") : Speed < HandRest ? TEXT("repos") : TEXT("mouvement");
|
||||
Hands->SetStringField(SideNames[Side], State);
|
||||
}
|
||||
Card->SetObjectField(TEXT("pieds_au_sol_pct"), Feet);
|
||||
Card->SetObjectField(TEXT("mains"), Hands);
|
||||
return Card;
|
||||
}
|
||||
}
|
||||
|
||||
ULPAnimCatalogCommandlet::ULPAnimCatalogCommandlet()
|
||||
{
|
||||
IsClient = false;
|
||||
IsServer = false;
|
||||
IsEditor = true;
|
||||
LogToConsole = true;
|
||||
}
|
||||
|
||||
int32 ULPAnimCatalogCommandlet::Main(const FString& Params)
|
||||
{
|
||||
using namespace LPAnimCatalogImpl;
|
||||
FString Livraison = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline/quaternius/livraison.json"));
|
||||
FParse::Value(*Params, TEXT("Livraison="), Livraison, false);
|
||||
FString Out = FPaths::GetPath(Livraison);
|
||||
FParse::Value(*Params, TEXT("Out="), Out, false);
|
||||
float SeuilBoucle = 1.f;
|
||||
float SeuilSurPlace = 5.f;
|
||||
FParse::Value(*Params, TEXT("SeuilBoucle="), SeuilBoucle);
|
||||
FParse::Value(*Params, TEXT("SeuilSurPlace="), SeuilSurPlace);
|
||||
|
||||
TSharedPtr<FJsonValue> Root;
|
||||
if (!ReadJson(Livraison, Root) || Root->Type != EJson::Array)
|
||||
{
|
||||
UE_LOG(LogLPAnimCatalog, Error, TEXT("livraison.json illisible : %s"), *Livraison);
|
||||
return 1;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Cards = MakeShared<FJsonObject>();
|
||||
TArray<FString> Failures;
|
||||
for (const TSharedPtr<FJsonValue>& Entry : Root->AsArray())
|
||||
{
|
||||
const TSharedPtr<FJsonObject> Item = Entry->AsObject();
|
||||
const FString Key = Item->GetStringField(TEXT("cle"));
|
||||
const FString PathF = Item->GetStringField(TEXT("source_F"));
|
||||
const FString PathM = Item->GetStringField(TEXT("source_M"));
|
||||
FClipInfo F, M;
|
||||
if (!ClipInfo(PathF, F) || !ClipInfo(PathM, M))
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : clip absent ou vide"), *Key));
|
||||
continue;
|
||||
}
|
||||
if (F.Frames != M.Frames || !FMath::IsNearlyEqual(F.Duration, M.Duration, 0.001f))
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : F %d images %.3f s, M %d images %.3f s"), *Key, F.Frames, F.Duration, M.Frames, M.Duration));
|
||||
}
|
||||
FString Error;
|
||||
TSharedRef<FJsonObject> Card = Measure(PathF, F, Error);
|
||||
if (!Error.IsEmpty())
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : %s"), *Key, *Error));
|
||||
continue;
|
||||
}
|
||||
Card->SetBoolField(TEXT("boucle"), Card->GetNumberField(TEXT("boucle_ecart_pct")) <= SeuilBoucle);
|
||||
if (Card->GetNumberField(TEXT("deplacement_cm")) < SeuilSurPlace)
|
||||
{
|
||||
Card->SetStringField(TEXT("deplacement_sens"), TEXT("sur place"));
|
||||
}
|
||||
UE_LOG(LogLPAnimCatalog, Display, TEXT("%-28s %5.2f s %3d img boucle %5.2f %% depl %4.0f cm %-9s posture %-9s (%s -> %s)"),
|
||||
*Key, F.Duration, F.Frames, Card->GetNumberField(TEXT("boucle_ecart_pct")), Card->GetNumberField(TEXT("deplacement_cm")),
|
||||
*Card->GetStringField(TEXT("deplacement_sens")), *Card->GetStringField(TEXT("posture")),
|
||||
*Card->GetStringField(TEXT("posture_debut")), *Card->GetStringField(TEXT("posture_fin")));
|
||||
Cards->SetObjectField(Key, Card);
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetStringField(TEXT("livraison"), Livraison);
|
||||
Report->SetNumberField(TEXT("seuil_boucle_pct"), SeuilBoucle);
|
||||
Report->SetNumberField(TEXT("seuil_sur_place_cm"), SeuilSurPlace);
|
||||
Report->SetObjectField(TEXT("fiches"), Cards);
|
||||
TArray<TSharedPtr<FJsonValue>> Errors;
|
||||
for (const FString& Failure : Failures)
|
||||
{
|
||||
Errors.Add(MakeShared<FJsonValueString>(Failure));
|
||||
UE_LOG(LogLPAnimCatalog, Error, TEXT("%s"), *Failure);
|
||||
}
|
||||
Report->SetArrayField(TEXT("echecs"), Errors);
|
||||
const FString OutPath = FPaths::Combine(Out, TEXT("fiches_calculees.json"));
|
||||
FString Text;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Text);
|
||||
FJsonSerializer::Serialize(Report, Writer);
|
||||
if (!FFileHelper::SaveStringToFile(Text, *OutPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogLPAnimCatalog, Error, TEXT("ecriture impossible : %s"), *OutPath);
|
||||
return 1;
|
||||
}
|
||||
UE_LOG(LogLPAnimCatalog, Warning, TEXT("CATALOGUE fiches %d echecs %d -> %s"), Cards->Values.Num(), Failures.Num(), *OutPath);
|
||||
return Failures.Num() > 0 ? 1 : 0;
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// Lot AP6 (Docs/Migration/20_Chaine_Animations.md, section 5) : champs calcules du catalogue des animations.
|
||||
//
|
||||
// Pour chaque cle de livraison.json (AP5), mesure le clip feminin source (copie identique de celui du mod) image
|
||||
// par image, et controle que le clip masculin a la meme duree et le meme nombre d'images. Mesures (corps du clip,
|
||||
// hauteurs depuis le sol du repos, en cm) :
|
||||
// duree, images, cadence ;
|
||||
// boucle : pose de la derniere image comparee a la premiere (mains, pieds, tete, relatives a la hanche),
|
||||
// ecart maximal en % de la taille (tete au repos) ; boucle si <= SeuilBoucle (1 %) ;
|
||||
// deplacement : trajet horizontal de la hanche entre premiere et derniere image (cm, cm/s) et sens par rapport a
|
||||
// l'avant du corps a la premiere image (avant, arriere, cote, sur place si < SeuilSurPlace) ;
|
||||
// posture : par image, allonge (tronc incline de plus de 60 deg), a genoux (genou a moins de 12 cm du sol,
|
||||
// hanche sous 72 % de sa hauteur debout), assis (hanche sous 72 %, en arriere des chevilles de plus
|
||||
// de 20 cm), accroupi (hanche sous 72 %, au-dessus des chevilles), debout sinon ; posture du clip =
|
||||
// celle d'au moins 60 % des images, sinon "variable" ; posture de debut et de fin ;
|
||||
// hauteur_assise_cm : hauteur mediane des articulations de hanche sur les images assises (le siege est plus bas
|
||||
// de l'epaisseur de la cuisse) ;
|
||||
// pieds : part des images ou chaque pied (cheville ou orteil) est a moins de 3 cm de sa hauteur de repos ;
|
||||
// mains : par main, repos (vitesse moyenne par rapport a la hanche < 15 cm/s), mouvement, levee (au-dessus
|
||||
// de la tete sur au moins 30 % des images).
|
||||
// Ecrit <Out>/fiches_calculees.json. Code 1 si un clip manque, est vide, ou si F et M different en duree ou en images.
|
||||
//
|
||||
// Usage :
|
||||
// UnrealEditor-Cmd.exe <projet> -run=LPAnimCatalog [-Livraison=<Saved/AnimPipeline/quaternius/livraison.json>]
|
||||
// [-Out=<dossier de livraison.json>] [-SeuilBoucle=1] [-SeuilSurPlace=5]
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "LPAnimCatalogCommandlet.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class ULPAnimCatalogCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULPAnimCatalogCommandlet();
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
#include "LPAnimChecks.h"
|
||||
|
||||
#include "Animation/AnimData/IAnimationDataModel.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "PhysicsEngine/BodySetup.h"
|
||||
#include "PhysicsEngine/PhysicsAsset.h"
|
||||
#include "PhysicsEngine/SkeletalBodySetup.h"
|
||||
#include "Rendering/SkeletalMeshLODModel.h"
|
||||
#include "Rendering/SkeletalMeshModel.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Os du tronc dont les capsules forment le volume teste (roles du profil).
|
||||
const TCHAR* const TrunkRoles[] = { TEXT("hips"), TEXT("pelvis"), TEXT("spine") };
|
||||
|
||||
// Seuils internes de detection (pas des seuils d'acceptation, fixes dans LPAnimEval).
|
||||
constexpr float ContactHeight = 3.f; // cm : pied source considere au sol
|
||||
constexpr float PlantedMove = 1.f; // cm par image : pied source immobile
|
||||
constexpr float StraightLeg = 15.f; // degres : jambe presque tendue
|
||||
constexpr float JumpFactor = 1.5f; // une rotation d'image peut depasser la source de 50 %
|
||||
constexpr float JumpMargin = 2.f; // ... plus 2 degres
|
||||
|
||||
int32 BoneOf(const FLPPose& Pose, const TCHAR* Role, int32 Index = 0)
|
||||
{
|
||||
const FName Name = LPPoseMeasure::RoleBone(Pose, Role, Index);
|
||||
return Name.IsNone() ? INDEX_NONE : Pose.Names.IndexOfByKey(Name);
|
||||
}
|
||||
|
||||
FVector At(const FLPPose& Pose, int32 Bone)
|
||||
{
|
||||
return Pose.Component.IsValidIndex(Bone) ? Pose.Component[Bone].GetLocation() : FVector::ZeroVector;
|
||||
}
|
||||
|
||||
// Avant du corps : perpendiculaire horizontale a l'axe des hanches.
|
||||
FVector ForwardOf(const FLPPose& Pose)
|
||||
{
|
||||
FVector Side = At(Pose, BoneOf(Pose, TEXT("thigh_l"))) - At(Pose, BoneOf(Pose, TEXT("thigh_r")));
|
||||
Side.Z = 0.f;
|
||||
return FVector::CrossProduct(Side.GetSafeNormal(), FVector::UpVector).GetSafeNormal();
|
||||
}
|
||||
|
||||
float Flexion(const FLPPose& Pose, int32 A, int32 B, int32 C)
|
||||
{
|
||||
const FVector Upper = (At(Pose, B) - At(Pose, A)).GetSafeNormal();
|
||||
const FVector Lower = (At(Pose, C) - At(Pose, B)).GetSafeNormal();
|
||||
return FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(Upper, Lower), -1.f, 1.f)));
|
||||
}
|
||||
|
||||
// Genou derriere la ligne hanche-cheville (cm, positif = hyperextension), jambe presque tendue seulement.
|
||||
float KneeBehind(const FLPPose& Pose, int32 Hip, int32 Knee, int32 Ankle)
|
||||
{
|
||||
if (Flexion(Pose, Hip, Knee, Ankle) > StraightLeg)
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
const FVector Line = (At(Pose, Ankle) - At(Pose, Hip)).GetSafeNormal();
|
||||
const FVector Offset = (At(Pose, Knee) - At(Pose, Hip)) - Line * FVector::DotProduct(At(Pose, Knee) - At(Pose, Hip), Line);
|
||||
return FMath::Max(0.f, -static_cast<float>(FVector::DotProduct(Offset, ForwardOf(Pose))));
|
||||
}
|
||||
|
||||
// Repli sans actif physique : une capsule par segment de la colonne (bassin -> base du cou), rayon pris sur
|
||||
// la peau du maillage (sommets dont l'os dominant est du tronc), au 30e centile des distances a l'axe :
|
||||
// prudent, les flancs plus larges que la profondeur ne declenchent pas de faux positifs.
|
||||
constexpr float SkinPercentile = 0.30f;
|
||||
|
||||
TArray<FLPCheckCapsule> SkinCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT, const TSet<int32>& Trunk)
|
||||
{
|
||||
TArray<FLPCheckCapsule> Capsules;
|
||||
const FSkeletalMeshModel* Imported = Mesh ? Mesh->GetImportedModel() : nullptr;
|
||||
if (!Imported || Imported->LODModels.Num() == 0 || BodyT.Rest.Num() != BodyT.Names.Num())
|
||||
{
|
||||
return Capsules;
|
||||
}
|
||||
const FReferenceSkeleton& MeshRef = Mesh->GetRefSkeleton();
|
||||
TArray<int32> Nodes;
|
||||
Nodes.Add(BoneOf(BodyT, TEXT("pelvis")));
|
||||
for (int32 Index = 0; ; ++Index)
|
||||
{
|
||||
const int32 Bone = BoneOf(BodyT, TEXT("spine"), Index);
|
||||
if (Bone == INDEX_NONE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Nodes.Add(Bone);
|
||||
}
|
||||
Nodes.Add(BoneOf(BodyT, TEXT("neck")));
|
||||
Nodes.RemoveAll([](int32 Bone) { return Bone == INDEX_NONE; });
|
||||
struct FSegment { int32 Bone; FVector A; FVector B; TArray<float> Distances; };
|
||||
TArray<FSegment> Segments;
|
||||
for (int32 Index = 0; Index + 1 < Nodes.Num(); ++Index)
|
||||
{
|
||||
const FVector A = BodyT.Rest[Nodes[Index]].GetLocation();
|
||||
const FVector B = BodyT.Rest[Nodes[Index + 1]].GetLocation();
|
||||
if (FVector::Dist(A, B) > 2.f)
|
||||
{
|
||||
Segments.Add({ Nodes[Index], A, B, {} });
|
||||
}
|
||||
}
|
||||
for (const FSkelMeshSection& Section : Imported->LODModels[0].Sections)
|
||||
{
|
||||
for (const FSoftSkinVertex& Vertex : Section.SoftVertices)
|
||||
{
|
||||
int32 Best = 0;
|
||||
for (int32 Influence = 1; Influence < MAX_TOTAL_INFLUENCES; ++Influence)
|
||||
{
|
||||
if (Vertex.InfluenceWeights[Influence] > Vertex.InfluenceWeights[Best])
|
||||
{
|
||||
Best = Influence;
|
||||
}
|
||||
}
|
||||
if (!Section.BoneMap.IsValidIndex(Vertex.InfluenceBones[Best]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int32 Bone = BodyT.Names.IndexOfByKey(MeshRef.GetBoneName(Section.BoneMap[Vertex.InfluenceBones[Best]]));
|
||||
if (!Trunk.Contains(Bone))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FVector P(Vertex.Position);
|
||||
for (FSegment& Segment : Segments)
|
||||
{
|
||||
const FVector AB = Segment.B - Segment.A;
|
||||
const float T = static_cast<float>(FVector::DotProduct(P - Segment.A, AB) / AB.SizeSquared());
|
||||
if (T >= 0.f && T <= 1.f)
|
||||
{
|
||||
Segment.Distances.Add(static_cast<float>(FVector::Dist(P, Segment.A + AB * T)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (FSegment& Segment : Segments)
|
||||
{
|
||||
if (Segment.Distances.Num() < 20)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Segment.Distances.Sort();
|
||||
const FTransform& Rest = BodyT.Rest[Segment.Bone];
|
||||
FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef();
|
||||
Capsule.Bone = Segment.Bone;
|
||||
Capsule.Center = Rest.InverseTransformPosition((Segment.A + Segment.B) * 0.5f);
|
||||
Capsule.Axis = Rest.InverseTransformVectorNoScale((Segment.B - Segment.A).GetSafeNormal());
|
||||
Capsule.HalfLength = static_cast<float>(FVector::Dist(Segment.A, Segment.B)) * 0.5f;
|
||||
Capsule.Radius = Segment.Distances[FMath::FloorToInt(Segment.Distances.Num() * SkinPercentile)];
|
||||
}
|
||||
return Capsules;
|
||||
}
|
||||
|
||||
float SegmentDistance(const FVector& P, const FVector& A, const FVector& B)
|
||||
{
|
||||
const FVector AB = B - A;
|
||||
const float T = AB.IsNearlyZero() ? 0.f : FMath::Clamp(static_cast<float>(FVector::DotProduct(P - A, AB) / AB.SizeSquared()), 0.f, 1.f);
|
||||
return static_cast<float>(FVector::Dist(P, A + AB * T));
|
||||
}
|
||||
}
|
||||
|
||||
namespace LPAnimChecks
|
||||
{
|
||||
TArray<FLPCheckCapsule> TrunkCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT)
|
||||
{
|
||||
TArray<FLPCheckCapsule> Capsules;
|
||||
const UPhysicsAsset* Physics = Mesh ? Mesh->GetPhysicsAsset() : nullptr;
|
||||
TSet<int32> Trunk;
|
||||
for (const TCHAR* Role : TrunkRoles)
|
||||
{
|
||||
for (int32 Index = 0; ; ++Index)
|
||||
{
|
||||
const int32 Bone = BoneOf(BodyT, Role, Index);
|
||||
if (Bone == INDEX_NONE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Trunk.Add(Bone);
|
||||
}
|
||||
}
|
||||
if (!Physics)
|
||||
{
|
||||
return SkinCapsules(Mesh, BodyT, Trunk);
|
||||
}
|
||||
for (const TObjectPtr<USkeletalBodySetup>& Setup : Physics->SkeletalBodySetups)
|
||||
{
|
||||
const int32 Bone = Setup ? BodyT.Names.IndexOfByKey(Setup->BoneName) : INDEX_NONE;
|
||||
if (Bone == INDEX_NONE || !Trunk.Contains(Bone))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const FKSphylElem& Sphyl : Setup->AggGeom.SphylElems)
|
||||
{
|
||||
FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef();
|
||||
Capsule.Bone = Bone;
|
||||
Capsule.Center = Sphyl.Center;
|
||||
Capsule.Axis = Sphyl.Rotation.Quaternion().GetAxisZ();
|
||||
Capsule.HalfLength = Sphyl.Length * 0.5f;
|
||||
Capsule.Radius = Sphyl.Radius;
|
||||
}
|
||||
for (const FKSphereElem& Sphere : Setup->AggGeom.SphereElems)
|
||||
{
|
||||
FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef();
|
||||
Capsule.Bone = Bone;
|
||||
Capsule.Center = Sphere.Center;
|
||||
Capsule.Radius = Sphere.Radius;
|
||||
}
|
||||
}
|
||||
return Capsules.Num() > 0 ? Capsules : SkinCapsules(Mesh, BodyT, Trunk);
|
||||
}
|
||||
|
||||
float SelfTest(const TArray<FLPCheckCapsule>& Trunk, const FLPPose& BodyT)
|
||||
{
|
||||
float Worst = Trunk.Num() > 0 ? 0.f : -1.f;
|
||||
for (const FLPCheckCapsule& Capsule : Trunk)
|
||||
{
|
||||
const FTransform& Bone = BodyT.Component[Capsule.Bone];
|
||||
const FVector Center = Bone.TransformPosition(Capsule.Center);
|
||||
const FVector Axis = Bone.TransformVectorNoScale(Capsule.Axis) * Capsule.HalfLength;
|
||||
// L'articulation de l'os porteur est une extremite du segment : profondeur egale au rayon si le centre
|
||||
// et l'axe ont ete bien ramenes dans le repere de l'os.
|
||||
const float Depth = Capsule.Radius - SegmentDistance(Bone.GetLocation(), Center - Axis, Center + Axis);
|
||||
Worst = FMath::Max(Worst, FMath::Abs(Depth - Capsule.Radius));
|
||||
}
|
||||
return Worst;
|
||||
}
|
||||
|
||||
FLPClipChecks CheckClip(const FString& RefPath, const FString& BodyPath, const FLPPose& RefT, const FLPPose& BodyT,
|
||||
const TArray<FLPCheckCapsule>& Trunk)
|
||||
{
|
||||
FLPClipChecks Out;
|
||||
Out.Clip = FPaths::GetBaseFilename(BodyPath);
|
||||
const UAnimSequence* Anim = LoadObject<UAnimSequence>(nullptr, *BodyPath);
|
||||
const IAnimationDataModel* Model = Anim ? Anim->GetDataModel() : nullptr;
|
||||
if (!Model)
|
||||
{
|
||||
Out.Error = TEXT("clip converti absent");
|
||||
return Out;
|
||||
}
|
||||
const FFrameRate Rate = Model->GetFrameRate();
|
||||
Out.Frames = Model->GetNumberOfKeys();
|
||||
|
||||
// Mise a l'echelle source -> corps : hauteur de hanche des deux poses T.
|
||||
const int32 RefHips = BoneOf(RefT, TEXT("hips"));
|
||||
const int32 BodyHips = BoneOf(BodyT, TEXT("hips"));
|
||||
const float Scale = RefHips != INDEX_NONE && BodyHips != INDEX_NONE && At(RefT, RefHips).Z > 1.f
|
||||
? static_cast<float>(At(BodyT, BodyHips).Z / At(RefT, RefHips).Z) : 1.f;
|
||||
|
||||
// Points de contact (pieds, orteils) et articulations (coude, genou), par role.
|
||||
const TCHAR* const ContactRoles[] = { TEXT("foot_l"), TEXT("toe_l"), TEXT("foot_r"), TEXT("toe_r") };
|
||||
struct FJoint { const TCHAR* A; const TCHAR* B; const TCHAR* C; bool bKnee; };
|
||||
const FJoint Joints[] = {
|
||||
{ TEXT("thigh_l"), TEXT("calf_l"), TEXT("foot_l"), true },
|
||||
{ TEXT("thigh_r"), TEXT("calf_r"), TEXT("foot_r"), true },
|
||||
{ TEXT("upperarm_l"), TEXT("lowerarm_l"), TEXT("hand_l"), false },
|
||||
{ TEXT("upperarm_r"), TEXT("lowerarm_r"), TEXT("hand_r"), false },
|
||||
};
|
||||
const TCHAR* const JumpRoles[] = {
|
||||
TEXT("hips"), TEXT("head"), TEXT("clavicle_l"), TEXT("upperarm_l"), TEXT("lowerarm_l"), TEXT("hand_l"),
|
||||
TEXT("clavicle_r"), TEXT("upperarm_r"), TEXT("lowerarm_r"), TEXT("hand_r"),
|
||||
TEXT("thigh_l"), TEXT("calf_l"), TEXT("foot_l"), TEXT("thigh_r"), TEXT("calf_r"), TEXT("foot_r")
|
||||
};
|
||||
const TCHAR* const ProbeRoles[] = { TEXT("hand_l"), TEXT("lowerarm_l"), TEXT("hand_r"), TEXT("lowerarm_r") };
|
||||
|
||||
FLPPose PrevRef;
|
||||
FLPPose PrevBody;
|
||||
for (int32 Key = 0; Key < Out.Frames; ++Key)
|
||||
{
|
||||
const float Time = static_cast<float>(Rate.AsSeconds(FFrameTime(Key)));
|
||||
FLPPose Ref;
|
||||
FLPPose Body;
|
||||
if (!LPPoseMeasure::Sample(RefPath, Time, Ref) || !LPPoseMeasure::Sample(BodyPath, Time, Body))
|
||||
{
|
||||
Out.Error = FString::Printf(TEXT("image %d illisible"), Key);
|
||||
return Out;
|
||||
}
|
||||
for (const TCHAR* Role : ContactRoles)
|
||||
{
|
||||
const int32 R = BoneOf(Ref, Role);
|
||||
const int32 B = BoneOf(Body, Role);
|
||||
if (R == INDEX_NONE || B == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Hauteur au-dessus de la pose T (pieds a plat), source a l'echelle du corps.
|
||||
const float RefHeight = static_cast<float>(At(Ref, R).Z - At(RefT, R).Z) * Scale;
|
||||
const float BodyHeight = static_cast<float>(At(Body, B).Z - At(BodyT, B).Z);
|
||||
const float Below = FMath::Max(0.f, -BodyHeight) - FMath::Max(0.f, -RefHeight);
|
||||
if (Below > Out.Floor)
|
||||
{
|
||||
Out.Floor = Below;
|
||||
Out.FloorBone = Body.Names[B].ToString();
|
||||
}
|
||||
if (Key > 0)
|
||||
{
|
||||
const FVector RefMove = (At(Ref, R) - At(PrevRef, R)) * Scale;
|
||||
const FVector BodyMove = At(Body, B) - At(PrevBody, B);
|
||||
const bool bPlanted = RefHeight < ContactHeight && FVector(RefMove.X, RefMove.Y, 0.f).Size() < PlantedMove;
|
||||
const float Slide = bPlanted ? static_cast<float>(FVector(BodyMove.X - RefMove.X, BodyMove.Y - RefMove.Y, 0.f).Size()) : 0.f;
|
||||
if (Slide > Out.Slide)
|
||||
{
|
||||
Out.Slide = Slide;
|
||||
Out.SlideBone = Body.Names[B].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const FJoint& Joint : Joints)
|
||||
{
|
||||
const int32 RA = BoneOf(Ref, Joint.A), RB = BoneOf(Ref, Joint.B), RC = BoneOf(Ref, Joint.C);
|
||||
const int32 BA = BoneOf(Body, Joint.A), BB = BoneOf(Body, Joint.B), BC = BoneOf(Body, Joint.C);
|
||||
if (RA == INDEX_NONE || RB == INDEX_NONE || RC == INDEX_NONE || BA == INDEX_NONE || BB == INDEX_NONE || BC == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float Gap = FMath::Abs(Flexion(Body, BA, BB, BC) - Flexion(Ref, RA, RB, RC));
|
||||
if (Gap > Out.FlexGap)
|
||||
{
|
||||
Out.FlexGap = Gap;
|
||||
Out.FlexJoint = Body.Names[BB].ToString();
|
||||
}
|
||||
if (Joint.bKnee)
|
||||
{
|
||||
const float Behind = KneeBehind(Body, BA, BB, BC) - KneeBehind(Ref, RA, RB, RC) * Scale;
|
||||
if (Behind > Out.KneeBack)
|
||||
{
|
||||
Out.KneeBack = Behind;
|
||||
Out.KneeBone = Body.Names[BB].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Key > 0)
|
||||
{
|
||||
for (const TCHAR* Role : JumpRoles)
|
||||
{
|
||||
const int32 R = BoneOf(Ref, Role);
|
||||
const int32 B = BoneOf(Body, Role);
|
||||
if (R == INDEX_NONE || B == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float RefTurn = FMath::RadiansToDegrees(Ref.Component[R].GetRotation().AngularDistance(PrevRef.Component[R].GetRotation()));
|
||||
const float BodyTurn = FMath::RadiansToDegrees(Body.Component[B].GetRotation().AngularDistance(PrevBody.Component[B].GetRotation()));
|
||||
const float Excess = BodyTurn - (RefTurn * JumpFactor + JumpMargin);
|
||||
if (Excess > Out.Jump)
|
||||
{
|
||||
Out.Jump = Excess;
|
||||
Out.JumpBone = Body.Names[B].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const TCHAR* Role : ProbeRoles)
|
||||
{
|
||||
const int32 B = BoneOf(Body, Role);
|
||||
if (B == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FVector Point = At(Body, B);
|
||||
for (const FLPCheckCapsule& Capsule : Trunk)
|
||||
{
|
||||
const FTransform& Bone = Body.Component[Capsule.Bone];
|
||||
const FVector Center = Bone.TransformPosition(Capsule.Center);
|
||||
const FVector Axis = Bone.TransformVectorNoScale(Capsule.Axis) * Capsule.HalfLength;
|
||||
const float Depth = Capsule.Radius - SegmentDistance(Point, Center - Axis, Center + Axis);
|
||||
if (Depth > Out.Penetration)
|
||||
{
|
||||
Out.Penetration = Depth;
|
||||
Out.PenetrationBone = Body.Names[B].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
PrevRef = MoveTemp(Ref);
|
||||
PrevBody = MoveTemp(Body);
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
// Lot AP4 (Docs/Migration/20_Chaine_Animations.md, section 4) : controles independants de la formule.
|
||||
//
|
||||
// Calcules image par image sur le clip converti, compares au mouvement source mis a l'echelle : un defaut deja
|
||||
// present dans le clip source n'est pas impute a la conversion. Aucun rendu.
|
||||
// sol : pied ou orteil plus bas que dans la source (cm, au-dela de la source) ;
|
||||
// glissement : pied qui bouge alors que le pied source est pose (cm par image) ;
|
||||
// genou : genou en hyperextension, derriere la ligne hanche-cheville, au-dela de la source (cm) ;
|
||||
// flexion : ecart d'angle de flexion du coude et du genou avec la source (degres) ;
|
||||
// saut : rotation d'un os d'une image a l'autre au-dela de celle de la source (degres) ;
|
||||
// penetration : main, poignet ou coude dans les capsules du tronc de l'actif physique du corps (cm).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
|
||||
class USkeletalMesh;
|
||||
|
||||
struct FLPCheckCapsule
|
||||
{
|
||||
int32 Bone = INDEX_NONE;
|
||||
// Dans l'espace de l'os : centre, axe (unitaire), demi-longueur du segment, rayon.
|
||||
FVector Center = FVector::ZeroVector;
|
||||
FVector Axis = FVector::UpVector;
|
||||
float HalfLength = 0.f;
|
||||
float Radius = 0.f;
|
||||
};
|
||||
|
||||
struct FLPClipChecks
|
||||
{
|
||||
FString Clip;
|
||||
int32 Frames = 0;
|
||||
float Floor = 0.f;
|
||||
FString FloorBone;
|
||||
float Slide = 0.f;
|
||||
FString SlideBone;
|
||||
float KneeBack = 0.f;
|
||||
FString KneeBone;
|
||||
float FlexGap = 0.f;
|
||||
FString FlexJoint;
|
||||
float Jump = 0.f;
|
||||
FString JumpBone;
|
||||
float Penetration = 0.f;
|
||||
FString PenetrationBone;
|
||||
FString Error;
|
||||
};
|
||||
|
||||
namespace LPAnimChecks
|
||||
{
|
||||
// Capsules du tronc (bassin, colonne, poitrine) de l'actif physique du maillage du corps.
|
||||
TArray<FLPCheckCapsule> TrunkCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT);
|
||||
|
||||
// Auto-test du calcul de penetration : l'articulation de l'os porteur de chaque capsule (extremite du segment),
|
||||
// sur le corps en pose T, doit etre a une profondeur egale au rayon. Rend l'ecart maximal (cm) ; -1 sans capsule.
|
||||
float SelfTest(const TArray<FLPCheckCapsule>& Trunk, const FLPPose& BodyT);
|
||||
|
||||
// Controle un clip converti (BodyPath) contre son clip source (RefPath), a chaque image du clip converti.
|
||||
FLPClipChecks CheckClip(const FString& RefPath, const FString& BodyPath, const FLPPose& RefT, const FLPPose& BodyT,
|
||||
const TArray<FLPCheckCapsule>& Trunk);
|
||||
}
|
||||
|
|
@ -1,550 +0,0 @@
|
|||
#include "LPAnimEvalCommandlet.h"
|
||||
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "LPAnimChecks.h"
|
||||
#include "LPAnimMethodSelect.h"
|
||||
#include "LPPoseMeasure.h"
|
||||
#include "LPSkeletonProfile.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogLPAnimEval, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FBody
|
||||
{
|
||||
FString Name;
|
||||
FString Prefix;
|
||||
};
|
||||
|
||||
FString ClipPath(const FString& Dir, const FString& Name)
|
||||
{
|
||||
return FString::Printf(TEXT("%s/%s.%s"), *Dir, *Name, *Name);
|
||||
}
|
||||
|
||||
TArray<FBody> ParseBodies(const FString& Text)
|
||||
{
|
||||
TArray<FBody> Bodies;
|
||||
TArray<FString> Items;
|
||||
Text.ParseIntoArray(Items, TEXT(","), true);
|
||||
for (const FString& Item : Items)
|
||||
{
|
||||
FString Name;
|
||||
FString Prefix;
|
||||
if (Item.Split(TEXT(":"), &Name, &Prefix))
|
||||
{
|
||||
Bodies.Add({ Name.TrimStartAndEnd(), Prefix.TrimStartAndEnd() });
|
||||
}
|
||||
}
|
||||
return Bodies;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
bool bCopy = false;
|
||||
bool bAll = true;
|
||||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
bCopy |= Name.StartsWith(Body.Prefix);
|
||||
bAll &= BodyNames.Contains(Body.Prefix + Name);
|
||||
}
|
||||
if (!bCopy && bAll)
|
||||
{
|
||||
Clips.Add(Name);
|
||||
}
|
||||
}
|
||||
Clips.Sort();
|
||||
return Clips;
|
||||
}
|
||||
|
||||
// Torsion : ecart de rotation (degres) des os Bend et Twist du profil entre le clip converti et la formule.
|
||||
// Les directions de segment ne voient pas la torsion d'un os autour de son axe ; cette mesure si.
|
||||
float TwistGap(const FLPPose& Raw, const FLPPose& Formula, FString& OutBone)
|
||||
{
|
||||
const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(Raw.Profile);
|
||||
float Worst = 0.f;
|
||||
if (!Profile)
|
||||
{
|
||||
return Worst;
|
||||
}
|
||||
for (const TCHAR* Role : { TEXT("upperarm"), TEXT("lowerarm"), TEXT("thigh") })
|
||||
{
|
||||
for (const TCHAR* Side : { TEXT("_l"), TEXT("_r") })
|
||||
{
|
||||
for (const FString& Name : { FString(Role) + Side, FString(Role) + TEXT("_twist") + Side })
|
||||
{
|
||||
const int32 Bone = Raw.Names.IndexOfByKey(Profile->Bone(Name));
|
||||
if (Bone == INDEX_NONE || !Formula.Component.IsValidIndex(Bone))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float Angle = FMath::RadiansToDegrees(
|
||||
Raw.Component[Bone].GetRotation().AngularDistance(Formula.Component[Bone].GetRotation()));
|
||||
if (Angle > Worst)
|
||||
{
|
||||
Worst = Angle;
|
||||
OutBone = Raw.Names[Bone].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Worst;
|
||||
}
|
||||
|
||||
bool SaveJson(const TSharedRef<FJsonObject>& Object, const FString& Path)
|
||||
{
|
||||
FString Text;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Text);
|
||||
FJsonSerializer::Serialize(Object, Writer);
|
||||
return FFileHelper::SaveStringToFile(Text, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
||||
}
|
||||
|
||||
// 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, float SeuilReglageDoigts, TArray<FString>& Failures)
|
||||
{
|
||||
const FString PosePath = FPaths::Combine(PoseDir, FString::Printf(TEXT("retarget_pose_%s.txt"), *Body.Name));
|
||||
FLPTFitResult Fit;
|
||||
if (!LPPoseMeasure::WriteRetargetPose(Reference, Template, PosePath) || !LPPoseMeasure::TFitResult(Reference, Template, Fit))
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : pose T non calculee"), *Body.Name));
|
||||
return false;
|
||||
}
|
||||
const int32 Before = Failures.Num();
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetStringField(TEXT("corps"), Body.Name);
|
||||
Report->SetStringField(TEXT("profil"), Template.Profile);
|
||||
Report->SetStringField(TEXT("fichier_pose"), PosePath);
|
||||
TArray<TSharedPtr<FJsonValue>> Segments;
|
||||
for (const FLPTFitSegment& Segment : Fit.Segments)
|
||||
{
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("segment"), Segment.Name);
|
||||
Item->SetNumberField(TEXT("avant"), Segment.Before);
|
||||
Item->SetNumberField(TEXT("apres"), Segment.After);
|
||||
Item->SetBoolField(TEXT("ajuste"), Segment.bFit);
|
||||
Segments.Add(MakeShared<FJsonValueObject>(Item));
|
||||
if (Segment.bFit && Segment.After > SeuilPoseT)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : segment %s %.2f %% apres ajustement (seuil %.2f)"), *Body.Name, *Segment.Name, Segment.After, SeuilPoseT));
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
Degrees.Add(Adjust.Bone, Adjust.Degrees);
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("os"), Adjust.Bone.ToString());
|
||||
Item->SetNumberField(TEXT("degres"), Adjust.Degrees);
|
||||
Adjusts.Add(MakeShared<FJsonValueObject>(Item));
|
||||
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, Bound));
|
||||
}
|
||||
}
|
||||
Report->SetArrayField(TEXT("reglages"), Adjusts);
|
||||
// Symetrie : chaque role _l face a son role _r, os par os.
|
||||
const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(Template.Profile);
|
||||
float WorstGap = 0.f;
|
||||
if (Profile)
|
||||
{
|
||||
for (const TPair<FString, TArray<FName>>& Role : Profile->Roles)
|
||||
{
|
||||
if (!Role.Key.EndsWith(TEXT("_l")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const TArray<FName>& Right = Profile->Chain(Role.Key.LeftChop(2) + TEXT("_r"));
|
||||
for (int32 Index = 0; Index < FMath::Min(Role.Value.Num(), Right.Num()); ++Index)
|
||||
{
|
||||
const float* Left = Degrees.Find(Role.Value[Index]);
|
||||
const float* Other = Degrees.Find(Right[Index]);
|
||||
const float Gap = FMath::Abs((Left ? *Left : 0.f) - (Other ? *Other : 0.f));
|
||||
WorstGap = FMath::Max(WorstGap, Gap);
|
||||
if (Gap > SeuilSymetrie)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : reglages %s / %s asymetriques de %.2f deg"), *Body.Name,
|
||||
*Role.Value[Index].ToString(), *Right[Index].ToString(), Gap));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Report->SetNumberField(TEXT("ecart_symetrie_max"), WorstGap);
|
||||
Report->SetBoolField(TEXT("valide"), Failures.Num() == Before);
|
||||
SaveJson(Report, FPaths::Combine(Out, FString::Printf(TEXT("rapport_poseT_%s.json"), *Body.Name)));
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL poseT %s profil %s : %d segments, %d reglages, symetrie %.3f deg, %s"),
|
||||
*Body.Name, *Template.Profile, Fit.Segments.Num(), Fit.Adjusts.Num(), WorstGap,
|
||||
Failures.Num() == Before ? TEXT("OK") : TEXT("ECHEC"));
|
||||
return Failures.Num() == Before;
|
||||
}
|
||||
}
|
||||
|
||||
ULPAnimEvalCommandlet::ULPAnimEvalCommandlet()
|
||||
{
|
||||
IsClient = false;
|
||||
IsEditor = true;
|
||||
IsServer = false;
|
||||
LogToConsole = true;
|
||||
}
|
||||
|
||||
int32 ULPAnimEvalCommandlet::Main(const FString& Params)
|
||||
{
|
||||
FString ClipDir = TEXT("/Game/Quaternius/UAL");
|
||||
FString RefClip = TEXT("A_TPose");
|
||||
FString BodiesText = TEXT("Bridget:B_,Michael:M_");
|
||||
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);
|
||||
FParse::Value(*Params, TEXT("RefClip="), RefClip);
|
||||
FParse::Value(*Params, TEXT("Bodies="), BodiesText, false);
|
||||
FParse::Value(*Params, TEXT("PoseDir="), PoseDir);
|
||||
FParse::Value(*Params, TEXT("Out="), Out);
|
||||
FParse::Value(*Params, TEXT("Clips="), ClipsText, false);
|
||||
FParse::Value(*Params, TEXT("Instants="), Instants);
|
||||
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) :
|
||||
// au-dela du seuil, le clip est "a revoir" (liste pour controle visuel) ; au-dela de RejetFacteur fois le
|
||||
// seuil, il est "rejete" et bloque la livraison.
|
||||
FString MeshesText = TEXT("Bridget:/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp,Michael:/Game/Heavy/Daz/Michael/michael.michael");
|
||||
FParse::Value(*Params, TEXT("Meshes="), MeshesText, false);
|
||||
const bool bChecks = !FParse::Param(*Params, TEXT("SansControles"));
|
||||
float SeuilSol = 3.f, SeuilGlissement = 3.f, SeuilGenou = 1.f, SeuilFlexion = 5.f, SeuilSaut = 10.f, SeuilPenetration = 1.f;
|
||||
float RejetFacteur = 3.f;
|
||||
FParse::Value(*Params, TEXT("RejetFacteur="), RejetFacteur);
|
||||
FParse::Value(*Params, TEXT("SeuilSol="), SeuilSol);
|
||||
FParse::Value(*Params, TEXT("SeuilGlissement="), SeuilGlissement);
|
||||
FParse::Value(*Params, TEXT("SeuilGenou="), SeuilGenou);
|
||||
FParse::Value(*Params, TEXT("SeuilFlexion="), SeuilFlexion);
|
||||
FParse::Value(*Params, TEXT("SeuilSaut="), SeuilSaut);
|
||||
FParse::Value(*Params, TEXT("SeuilPenetration="), SeuilPenetration);
|
||||
TMap<FString, FString> Meshes;
|
||||
{
|
||||
TArray<FString> Items;
|
||||
MeshesText.ParseIntoArray(Items, TEXT(","), true);
|
||||
for (const FString& Item : Items)
|
||||
{
|
||||
FString Name, Path;
|
||||
if (Item.Split(TEXT(":"), &Name, &Path))
|
||||
{
|
||||
Meshes.Add(Name, Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Instants = FMath::Max(Instants, 1);
|
||||
IFileManager::Get().MakeDirectory(*Out, true);
|
||||
IFileManager::Get().MakeDirectory(*PoseDir, true);
|
||||
const TArray<FBody> Bodies = ParseBodies(BodiesText);
|
||||
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())
|
||||
{
|
||||
UE_LOG(LogLPAnimEval, Error, TEXT("ANIMEVAL reference absente ou squelette inconnu : %s"), *ClipPath(ClipDir, RefClip));
|
||||
return 1;
|
||||
}
|
||||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
FLPPose Template;
|
||||
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, 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, BodyClipDir, Bodies);
|
||||
if (!ClipsText.IsEmpty())
|
||||
{
|
||||
TArray<FString> Wanted;
|
||||
ClipsText.ParseIntoArray(Wanted, TEXT(","), true);
|
||||
Clips = Clips.FilterByPredicate([&Wanted](const FString& Clip) { return Wanted.Contains(Clip); });
|
||||
}
|
||||
Summary->SetNumberField(TEXT("clips"), Clips.Num());
|
||||
TArray<TSharedPtr<FJsonValue>> BodyReports;
|
||||
TArray<TSharedPtr<FJsonValue>> ToReview;
|
||||
for (const FBody& Body : Bodies)
|
||||
{
|
||||
TArray<FString> Csv;
|
||||
Csv.Add(TEXT("clip,instant,formule_pire,formule_os,brut_pire,brut_os,torsion_deg,torsion_os"));
|
||||
float WorstFormula = 0.f;
|
||||
float WorstTwist = 0.f;
|
||||
float WorstRaw = 0.f;
|
||||
int32 Rejected = 0;
|
||||
for (const FString& Clip : Clips)
|
||||
{
|
||||
const FString RefPath = ClipPath(ClipDir, Clip);
|
||||
const UAnimSequence* Anim = LoadObject<UAnimSequence>(nullptr, *RefPath);
|
||||
const float Length = Anim ? Anim->GetPlayLength() : 0.f;
|
||||
for (int32 Step = 0; Step < Instants; ++Step)
|
||||
{
|
||||
// Meme instant que la tournee de la sandbox (k / Instants de la duree).
|
||||
const float Fraction = static_cast<float>(Step) / static_cast<float>(Instants);
|
||||
const float Time = FMath::Clamp(Fraction, 0.f, 1.f) * Length;
|
||||
FLPPose RefPose;
|
||||
FLPPose Raw;
|
||||
FLPPose Formula;
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
const FLPPoseMeasureReport FormulaCheck = LPPoseMeasure::Compare(*Body.Name, *Clip, RefPose, Formula);
|
||||
const FLPPoseMeasureReport RawCheck = LPPoseMeasure::Compare(*Body.Name, *Clip, RefPose, Raw);
|
||||
FString TwistBone;
|
||||
const float Twist = TwistGap(Raw, Formula, TwistBone);
|
||||
WorstTwist = FMath::Max(WorstTwist, Twist);
|
||||
Csv.Add(FString::Printf(TEXT("%s,%.2f,%.2f,%s,%.2f,%s,%.2f,%s"), *Clip, Fraction,
|
||||
FormulaCheck.WorstAnglePercent, *FormulaCheck.WorstAngleBone, RawCheck.WorstAnglePercent, *RawCheck.WorstAngleBone,
|
||||
Twist, *TwistBone));
|
||||
if (SeuilTorsion >= 0.f && Twist > SeuilTorsion)
|
||||
{
|
||||
++Rejected;
|
||||
Failures.Add(FString::Printf(TEXT("%s : %s a %.2f torsion %.2f deg (%s), seuil %.2f"), *Body.Name, *Clip, Fraction, Twist, *TwistBone, SeuilTorsion));
|
||||
}
|
||||
WorstFormula = FMath::Max(WorstFormula, FormulaCheck.WorstAnglePercent);
|
||||
WorstRaw = FMath::Max(WorstRaw, RawCheck.WorstAnglePercent);
|
||||
if (FormulaCheck.WorstAnglePercent > SeuilEval || RawCheck.WorstAnglePercent > SeuilEval)
|
||||
{
|
||||
++Rejected;
|
||||
Failures.Add(FString::Printf(TEXT("%s : %s a %.2f formule %.2f %% (%s), clip %.2f %% (%s)"), *Body.Name, *Clip, Fraction,
|
||||
FormulaCheck.WorstAnglePercent, *FormulaCheck.WorstAngleBone, RawCheck.WorstAnglePercent, *RawCheck.WorstAngleBone));
|
||||
}
|
||||
}
|
||||
}
|
||||
FFileHelper::SaveStringArrayToFile(Csv, *FPaths::Combine(Out, FString::Printf(TEXT("eval_%s.csv"), *Body.Name)));
|
||||
|
||||
// Controles independants, a chaque image de chaque clip converti.
|
||||
int32 ChecksFlagged = 0;
|
||||
if (bChecks)
|
||||
{
|
||||
FLPPose Template, RefT, BodyT;
|
||||
const USkeletalMesh* Mesh = Meshes.Contains(Body.Name) ? LoadObject<USkeletalMesh>(nullptr, *Meshes[Body.Name]) : nullptr;
|
||||
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"),
|
||||
*Body.Name, Mesh ? *Mesh->GetName() : TEXT("ABSENT"), Trunk.Num());
|
||||
const float SelfGap = LPAnimChecks::SelfTest(Trunk, BodyT);
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL capsule auto-test %s : ecart %.4f cm"), *Body.Name, SelfGap);
|
||||
if (SelfGap < 0.f || SelfGap > 0.01f)
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : auto-test de penetration en echec (%.4f cm)"), *Body.Name, SelfGap));
|
||||
}
|
||||
for (const FLPCheckCapsule& Capsule : Trunk)
|
||||
{
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL capsule %s %s : rayon %.1f cm, demi-longueur %.1f cm"),
|
||||
*Body.Name, *BodyT.Names[Capsule.Bone].ToString(), Capsule.Radius, Capsule.HalfLength);
|
||||
}
|
||||
TArray<FString> Lines;
|
||||
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(BodyClipDir, Body.Prefix + Clip), RefT, BodyT, Trunk);
|
||||
TArray<FString> Over;
|
||||
bool bReject = !C.Error.IsEmpty();
|
||||
if (bReject) { Over.Add(C.Error); }
|
||||
auto Test = [&](float Value, float Seuil, const TCHAR* Label, const FString& Bone)
|
||||
{
|
||||
if (Value > Seuil)
|
||||
{
|
||||
Over.Add(FString::Printf(TEXT("%s %.1f (%s)"), Label, Value, *Bone));
|
||||
bReject |= Value > Seuil * RejetFacteur;
|
||||
}
|
||||
};
|
||||
Test(C.Floor, SeuilSol, TEXT("sol cm"), C.FloorBone);
|
||||
Test(C.Slide, SeuilGlissement, TEXT("glissement cm"), C.SlideBone);
|
||||
Test(C.KneeBack, SeuilGenou, TEXT("genou cm"), C.KneeBone);
|
||||
Test(C.FlexGap, SeuilFlexion, TEXT("flexion deg"), C.FlexJoint);
|
||||
Test(C.Jump, SeuilSaut, TEXT("saut deg"), C.JumpBone);
|
||||
Test(C.Penetration, SeuilPenetration, TEXT("penetration cm"), C.PenetrationBone);
|
||||
const FString Verdict = Over.Num() == 0 ? TEXT("accepte") : bReject ? TEXT("rejete") : TEXT("a revoir");
|
||||
Lines.Add(FString::Printf(TEXT("%s,%d,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%s"), *Clip, C.Frames,
|
||||
C.Floor, *C.FloorBone, C.Slide, *C.SlideBone, C.KneeBack, *C.KneeBone, C.FlexGap, *C.FlexJoint,
|
||||
C.Jump, *C.JumpBone, C.Penetration, *C.PenetrationBone, *Verdict));
|
||||
if (Over.Num() > 0)
|
||||
{
|
||||
const FString Detail = FString::Printf(TEXT("%s : %s : %s"), *Body.Name, *Clip, *FString::Join(Over, TEXT(", ")));
|
||||
if (bReject)
|
||||
{
|
||||
Failures.Add(Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
++ChecksFlagged;
|
||||
ToReview.Add(MakeShared<FJsonValueString>(Detail));
|
||||
}
|
||||
}
|
||||
}
|
||||
FFileHelper::SaveStringArrayToFile(Lines, *FPaths::Combine(Out, FString::Printf(TEXT("controles_%s.csv"), *Body.Name)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Failures.Add(FString::Printf(TEXT("%s : controles impossibles (pose T)"), *Body.Name));
|
||||
}
|
||||
}
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("corps"), Body.Name);
|
||||
Item->SetNumberField(TEXT("instants_mesures"), Csv.Num() - 1);
|
||||
Item->SetNumberField(TEXT("formule_pire"), WorstFormula);
|
||||
Item->SetNumberField(TEXT("clip_pire"), WorstRaw);
|
||||
Item->SetNumberField(TEXT("torsion_pire_deg"), WorstTwist);
|
||||
Item->SetNumberField(TEXT("instants_hors_seuil"), Rejected);
|
||||
Item->SetNumberField(TEXT("clips_a_revoir_controles"), ChecksFlagged);
|
||||
BodyReports.Add(MakeShared<FJsonValueObject>(Item));
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL eval %s : %d instants, formule pire %.2f %%, clip pire %.2f %%, torsion pire %.2f deg, %d hors seuil"),
|
||||
*Body.Name, Csv.Num() - 1, WorstFormula, WorstRaw, WorstTwist, Rejected);
|
||||
}
|
||||
Summary->SetArrayField(TEXT("corps"), BodyReports);
|
||||
TArray<TSharedPtr<FJsonValue>> FailureList;
|
||||
for (const FString& Failure : Failures)
|
||||
{
|
||||
FailureList.Add(MakeShared<FJsonValueString>(Failure));
|
||||
UE_LOG(LogLPAnimEval, Warning, TEXT("ANIMEVAL echec %s"), *Failure);
|
||||
}
|
||||
Summary->SetArrayField(TEXT("echecs"), FailureList);
|
||||
// Clips a revoir : n'empechent pas la livraison, a regarder dans la sandbox (planches de la tournee).
|
||||
Summary->SetArrayField(TEXT("a_revoir"), ToReview);
|
||||
for (const TSharedPtr<FJsonValue>& Item : ToReview)
|
||||
{
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL a revoir %s"), *Item->AsString());
|
||||
}
|
||||
Summary->SetBoolField(TEXT("valide"), Failures.Num() == 0);
|
||||
SaveJson(Summary, FPaths::Combine(Out, TEXT("rapport.json")));
|
||||
UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL resultat %s : %d echec(s), rapports dans %s"),
|
||||
Failures.Num() == 0 ? TEXT("VALIDE") : TEXT("REFUSE"), Failures.Num(), *Out);
|
||||
return Failures.Num() == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// 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/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),
|
||||
// <Out>/rapport.json. Code de sortie 0 si tous les controles passent, 1 sinon.
|
||||
//
|
||||
// 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] [-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
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "LPAnimEvalCommandlet.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class ULPAnimEvalCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULPAnimEvalCommandlet();
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// 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;
|
||||
};
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,398 +0,0 @@
|
|||
#include "LPAnimTwistCommandlet.h"
|
||||
|
||||
#include "Animation/AnimData/IAnimationDataController.h"
|
||||
#include "Animation/AnimData/IAnimationDataModel.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#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"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogLPAnimTwist, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
// Tolerances du controle de conservation : un os hors des paires ne doit pas bouger.
|
||||
constexpr float KeepDegrees = 0.01f;
|
||||
constexpr float KeepCentimeters = 0.01f;
|
||||
|
||||
struct FTwistPair
|
||||
{
|
||||
int32 Bend = INDEX_NONE;
|
||||
int32 Twist = INDEX_NONE;
|
||||
int32 AxisChild = INDEX_NONE;
|
||||
// Axe du segment dans le repere du parent en pose T ; rotation locale de l'os Bend en pose T ;
|
||||
// rotation locale de l'os Twist (par rapport a l'os Bend) en pose T.
|
||||
FVector AxisParent = FVector::ZeroVector;
|
||||
FQuat BendLocalT = FQuat::Identity;
|
||||
FQuat TwistLocalT = FQuat::Identity;
|
||||
};
|
||||
|
||||
struct FClipResult
|
||||
{
|
||||
FString Clip;
|
||||
int32 Keys = 0;
|
||||
float TwistMoved = 0.f;
|
||||
float KeepAngle = 0.f;
|
||||
float KeepDistance = 0.f;
|
||||
bool bSaved = false;
|
||||
FString Error;
|
||||
};
|
||||
|
||||
FString ClipPath(const FString& Dir, const FString& Name)
|
||||
{
|
||||
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;
|
||||
const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(BodyT.Profile);
|
||||
if (!Profile)
|
||||
{
|
||||
return Pairs;
|
||||
}
|
||||
struct FRoles { const TCHAR* Bend; const TCHAR* Twist; const TCHAR* Axis; };
|
||||
const FRoles Roles[] = {
|
||||
{ TEXT("upperarm"), TEXT("upperarm_twist"), TEXT("lowerarm") },
|
||||
{ TEXT("lowerarm"), TEXT("lowerarm_twist"), TEXT("hand") },
|
||||
{ TEXT("thigh"), TEXT("thigh_twist"), TEXT("calf") },
|
||||
};
|
||||
for (const TCHAR* Side : { TEXT("_l"), TEXT("_r") })
|
||||
{
|
||||
for (const FRoles& Role : Roles)
|
||||
{
|
||||
FTwistPair Pair;
|
||||
Pair.Bend = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Bend) + Side));
|
||||
Pair.Twist = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Twist) + Side));
|
||||
Pair.AxisChild = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Axis) + Side));
|
||||
if (Pair.Bend == INDEX_NONE || Pair.Twist == INDEX_NONE || Pair.AxisChild == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int32 Parent = BodyT.Parents[Pair.Bend];
|
||||
if (Parent == INDEX_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FQuat ParentT = BodyT.Component[Parent].GetRotation();
|
||||
const FVector AxisT = (BodyT.Component[Pair.AxisChild].GetLocation() - BodyT.Component[Pair.Bend].GetLocation()).GetSafeNormal();
|
||||
Pair.AxisParent = ParentT.UnrotateVector(AxisT);
|
||||
Pair.BendLocalT = ParentT.Inverse() * BodyT.Component[Pair.Bend].GetRotation();
|
||||
Pair.TwistLocalT = BodyT.Component[Pair.Bend].GetRotation().Inverse() * BodyT.Component[Pair.Twist].GetRotation();
|
||||
if (!AxisT.IsNearlyZero())
|
||||
{
|
||||
Pairs.Add(Pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pairs;
|
||||
}
|
||||
|
||||
// Traite un clip : cles locales -> composant, swing / twist, nouvelles cles locales des os modifies.
|
||||
FClipResult ProcessClip(UAnimSequence* Sequence, const FLPPose& BodyT, const TArray<FTwistPair>& Pairs, bool bDryRun)
|
||||
{
|
||||
FClipResult Result;
|
||||
Result.Clip = Sequence->GetName();
|
||||
const USkeleton* Skeleton = Sequence->GetSkeleton();
|
||||
const IAnimationDataModel* Model = Sequence->GetDataModel();
|
||||
if (!Skeleton || !Model)
|
||||
{
|
||||
Result.Error = TEXT("squelette ou donnees absents");
|
||||
return Result;
|
||||
}
|
||||
const FReferenceSkeleton& Ref = Skeleton->GetReferenceSkeleton();
|
||||
const int32 Num = Ref.GetNum();
|
||||
if (Num != BodyT.Names.Num())
|
||||
{
|
||||
Result.Error = TEXT("squelette different du corps de reference");
|
||||
return Result;
|
||||
}
|
||||
const int32 Keys = Model->GetNumberOfKeys();
|
||||
Result.Keys = Keys;
|
||||
TArray<TArray<FTransform>> Local;
|
||||
Local.SetNum(Num);
|
||||
for (int32 Bone = 0; Bone < Num; ++Bone)
|
||||
{
|
||||
const FName Name = Ref.GetBoneName(Bone);
|
||||
if (Model->IsValidBoneTrackName(Name))
|
||||
{
|
||||
Model->GetBoneTrackTransforms(Name, Local[Bone]);
|
||||
}
|
||||
if (Local[Bone].Num() != Keys)
|
||||
{
|
||||
Local[Bone].Init(Ref.GetRefBonePose()[Bone], Keys);
|
||||
}
|
||||
}
|
||||
TSet<int32> Changed;
|
||||
TArray<TArray<FTransform>> NewLocal = Local;
|
||||
for (int32 Key = 0; Key < Keys; ++Key)
|
||||
{
|
||||
TArray<FTransform> Comp;
|
||||
Comp.SetNum(Num);
|
||||
for (int32 Bone = 0; Bone < Num; ++Bone)
|
||||
{
|
||||
const int32 Parent = Ref.GetParentIndex(Bone);
|
||||
Comp[Bone] = Parent == INDEX_NONE ? Local[Bone][Key] : Local[Bone][Key] * Comp[Parent];
|
||||
}
|
||||
for (const FTwistPair& Pair : Pairs)
|
||||
{
|
||||
// Rotation complete du membre : portee par l'os Twist (sa rotation locale de pose T retiree), que le
|
||||
// clip sorte du retargeter ou ait deja ete traite. Decomposition dans le repere du parent (AP4).
|
||||
const int32 Parent = Ref.GetParentIndex(Pair.Bend);
|
||||
const FQuat ParentNow = Comp[Parent].GetRotation();
|
||||
const FQuat Full = Comp[Pair.Twist].GetRotation() * Pair.TwistLocalT.Inverse();
|
||||
const FQuat Delta = ((ParentNow.Inverse() * Full) * Pair.BendLocalT.Inverse()).GetNormalized();
|
||||
FQuat Swing;
|
||||
FQuat Twist;
|
||||
Delta.ToSwingTwist(Pair.AxisParent, Swing, Twist);
|
||||
const FQuat Before = Comp[Pair.Bend].GetRotation();
|
||||
FTransform BendNew = Comp[Pair.Bend];
|
||||
BendNew.SetRotation((ParentNow * Swing * Pair.BendLocalT).GetNormalized());
|
||||
Result.TwistMoved = FMath::Max(Result.TwistMoved, FMath::RadiansToDegrees(Before.AngularDistance(BendNew.GetRotation())));
|
||||
FTransform& BendLocal = NewLocal[Pair.Bend][Key];
|
||||
BendLocal.SetRotation((ParentNow.Inverse() * BendNew.GetRotation()).GetNormalized());
|
||||
Changed.Add(Pair.Bend);
|
||||
// Enfants directs de l'os Bend (dont l'os Twist) : transformation composant conservee.
|
||||
for (int32 Child = 0; Child < Num; ++Child)
|
||||
{
|
||||
if (Ref.GetParentIndex(Child) != Pair.Bend)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FTransform& ChildLocal = NewLocal[Child][Key];
|
||||
ChildLocal.SetRotation((BendNew.GetRotation().Inverse() * Comp[Child].GetRotation()).GetNormalized());
|
||||
ChildLocal.SetTranslation(BendNew.GetRotation().UnrotateVector(Comp[Child].GetLocation() - BendNew.GetLocation()));
|
||||
Changed.Add(Child);
|
||||
}
|
||||
}
|
||||
// Controle : toute la chaine recomposee avec les nouvelles cles, hors os Bend, doit etre inchangee.
|
||||
TArray<FTransform> Check;
|
||||
Check.SetNum(Num);
|
||||
for (int32 Bone = 0; Bone < Num; ++Bone)
|
||||
{
|
||||
const int32 Parent = Ref.GetParentIndex(Bone);
|
||||
Check[Bone] = Parent == INDEX_NONE ? NewLocal[Bone][Key] : NewLocal[Bone][Key] * Check[Parent];
|
||||
bool bBend = false;
|
||||
for (const FTwistPair& Pair : Pairs)
|
||||
{
|
||||
bBend |= Pair.Bend == Bone;
|
||||
}
|
||||
if (!bBend)
|
||||
{
|
||||
Result.KeepAngle = FMath::Max(Result.KeepAngle, FMath::RadiansToDegrees(Check[Bone].GetRotation().AngularDistance(Comp[Bone].GetRotation())));
|
||||
Result.KeepDistance = FMath::Max(Result.KeepDistance, static_cast<float>(FVector::Dist(Check[Bone].GetLocation(), Comp[Bone].GetLocation())));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Result.KeepAngle > KeepDegrees || Result.KeepDistance > KeepCentimeters)
|
||||
{
|
||||
Result.Error = FString::Printf(TEXT("conservation hors tolerance : %.4f deg, %.4f cm"), Result.KeepAngle, Result.KeepDistance);
|
||||
return Result;
|
||||
}
|
||||
if (bDryRun)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
IAnimationDataController& Controller = Sequence->GetController();
|
||||
Controller.OpenBracket(FText::FromString(TEXT("Repartition de la torsion Bend / Twist (AP3)")), false);
|
||||
for (const int32 Bone : Changed)
|
||||
{
|
||||
const FName Name = Ref.GetBoneName(Bone);
|
||||
if (!Model->IsValidBoneTrackName(Name))
|
||||
{
|
||||
Controller.AddBoneCurve(Name, false);
|
||||
}
|
||||
TArray<FVector> Positions;
|
||||
TArray<FQuat> Rotations;
|
||||
TArray<FVector> Scales;
|
||||
for (const FTransform& Xform : NewLocal[Bone])
|
||||
{
|
||||
Positions.Add(Xform.GetTranslation());
|
||||
Rotations.Add(Xform.GetRotation());
|
||||
Scales.Add(Xform.GetScale3D());
|
||||
}
|
||||
Controller.SetBoneTrackKeys(Name, Positions, Rotations, Scales, false);
|
||||
}
|
||||
Controller.CloseBracket(false);
|
||||
UPackage* Package = Sequence->GetPackage();
|
||||
Package->MarkPackageDirty();
|
||||
FSavePackageArgs Args;
|
||||
Args.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
const FString File = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
|
||||
Result.bSaved = UPackage::SavePackage(Package, Sequence, *File, Args);
|
||||
if (!Result.bSaved)
|
||||
{
|
||||
Result.Error = TEXT("sauvegarde impossible");
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
|
||||
ULPAnimTwistCommandlet::ULPAnimTwistCommandlet()
|
||||
{
|
||||
IsClient = false;
|
||||
IsEditor = true;
|
||||
IsServer = false;
|
||||
LogToConsole = true;
|
||||
}
|
||||
|
||||
int32 ULPAnimTwistCommandlet::Main(const FString& Params)
|
||||
{
|
||||
FString ClipDir = TEXT("/Game/Quaternius/UAL");
|
||||
FString RefClip = TEXT("A_TPose");
|
||||
FString BodiesText = TEXT("Bridget:B_,Michael:M_");
|
||||
FString ClipsText;
|
||||
FString Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("quaternius"));
|
||||
FParse::Value(*Params, TEXT("ClipDir="), ClipDir);
|
||||
FParse::Value(*Params, TEXT("RefClip="), RefClip);
|
||||
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))
|
||||
{
|
||||
UE_LOG(LogLPAnimTwist, Error, TEXT("TORSION reference absente %s"), *ClipPath(ClipDir, RefClip));
|
||||
return 1;
|
||||
}
|
||||
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||||
Registry.ScanPathsSynchronous({ BodyClipDir }, true);
|
||||
TArray<FAssetData> Assets;
|
||||
Registry.GetAssetsByPath(FName(*BodyClipDir), Assets, false);
|
||||
TArray<FString> Wanted;
|
||||
ClipsText.ParseIntoArray(Wanted, TEXT(","), true);
|
||||
|
||||
TArray<FString> BodyItems;
|
||||
BodiesText.ParseIntoArray(BodyItems, TEXT(","), true);
|
||||
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)
|
||||
{
|
||||
FString BodyName;
|
||||
FString Prefix;
|
||||
if (!Item.Split(TEXT(":"), &BodyName, &Prefix))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FLPPose Template;
|
||||
FLPPose RefT;
|
||||
FLPPose BodyT;
|
||||
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);
|
||||
++Failures;
|
||||
continue;
|
||||
}
|
||||
const TArray<FTwistPair> Pairs = PairsFor(BodyT);
|
||||
UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION %s profil %s : %d paires Bend / Twist"), *BodyName, *BodyT.Profile, Pairs.Num());
|
||||
TArray<TSharedPtr<FJsonValue>> ClipReports;
|
||||
float WorstMoved = 0.f;
|
||||
for (const FAssetData& Asset : Assets)
|
||||
{
|
||||
const FString Name = Asset.AssetName.ToString();
|
||||
if (Asset.AssetClassPath != UAnimSequence::StaticClass()->GetClassPathName() || !Name.StartsWith(Prefix)
|
||||
|| (Wanted.Num() > 0 && !Wanted.Contains(Name.RightChop(Prefix.Len()))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UAnimSequence* Sequence = LoadObject<UAnimSequence>(nullptr, *ClipPath(BodyClipDir, Name));
|
||||
if (!Sequence)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FClipResult Result = ProcessClip(Sequence, BodyT, Pairs, bDryRun);
|
||||
WorstMoved = FMath::Max(WorstMoved, Result.TwistMoved);
|
||||
TSharedRef<FJsonObject> Clip = MakeShared<FJsonObject>();
|
||||
Clip->SetStringField(TEXT("clip"), Result.Clip);
|
||||
Clip->SetNumberField(TEXT("cles"), Result.Keys);
|
||||
Clip->SetNumberField(TEXT("torsion_deplacee_deg"), Result.TwistMoved);
|
||||
Clip->SetNumberField(TEXT("conservation_deg"), Result.KeepAngle);
|
||||
Clip->SetNumberField(TEXT("conservation_cm"), Result.KeepDistance);
|
||||
Clip->SetBoolField(TEXT("sauve"), Result.bSaved);
|
||||
Clip->SetStringField(TEXT("erreur"), Result.Error);
|
||||
ClipReports.Add(MakeShared<FJsonValueObject>(Clip));
|
||||
if (!Result.Error.IsEmpty())
|
||||
{
|
||||
++Failures;
|
||||
UE_LOG(LogLPAnimTwist, Warning, TEXT("TORSION echec %s : %s"), *Result.Clip, *Result.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION %s %d cles, torsion deplacee %.1f deg, conservation %.5f deg %.5f cm%s"),
|
||||
*Result.Clip, Result.Keys, Result.TwistMoved, Result.KeepAngle, Result.KeepDistance, bDryRun ? TEXT(" (essai)") : TEXT(""));
|
||||
}
|
||||
}
|
||||
TSharedRef<FJsonObject> Body = MakeShared<FJsonObject>();
|
||||
Body->SetStringField(TEXT("corps"), BodyName);
|
||||
Body->SetNumberField(TEXT("paires"), Pairs.Num());
|
||||
Body->SetNumberField(TEXT("torsion_deplacee_max_deg"), WorstMoved);
|
||||
Body->SetArrayField(TEXT("clips"), ClipReports);
|
||||
BodyReports.Add(MakeShared<FJsonValueObject>(Body));
|
||||
}
|
||||
Report->SetArrayField(TEXT("corps"), BodyReports);
|
||||
Report->SetNumberField(TEXT("echecs"), Failures);
|
||||
Report->SetBoolField(TEXT("valide"), Failures == 0);
|
||||
FString Text;
|
||||
FJsonSerializer::Serialize(Report, TJsonWriterFactory<>::Create(&Text));
|
||||
FFileHelper::SaveStringToFile(Text, *FPaths::Combine(Out, TEXT("rapport_torsion.json")), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
||||
UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION resultat %s : %d echec(s)"), Failures == 0 ? TEXT("VALIDE") : TEXT("REFUSE"), Failures);
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Lot AP3 (Docs/Migration/20_Chaine_Animations.md) : repartition de la torsion entre os Bend et Twist.
|
||||
//
|
||||
// Le retargeter Unreal met toute la rotation d'un membre dans l'os Bend (les os Twist sont hors chaine) :
|
||||
// la peau de l'epaule, de l'avant-bras et de la cuisse se vrille. Pour chaque cle des clips convertis, la
|
||||
// rotation de l'os Bend depuis la pose T ajustee est decomposee autour de l'axe du segment (swing / twist) :
|
||||
// l'os Bend garde la flexion, l'os Twist recoit la rotation complete. Tous les autres os gardent exactement
|
||||
// leur transformation (controle a chaque cle) ; c'est la repartition de la formule (LPPoseMeasure::TransferPose).
|
||||
// Les paires viennent du profil du corps (roles upperarm / upperarm_twist / lowerarm, etc.).
|
||||
//
|
||||
// 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
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "LPAnimTwistCommandlet.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class ULPAnimTwistCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULPAnimTwistCommandlet();
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
// Portage UE 5.5 - voir LPHairFromStaticMeshCommandlet.h
|
||||
|
||||
#include "LPHairFromStaticMeshCommandlet.h"
|
||||
|
||||
#include "AnimationRuntime.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "MeshDescription.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Rendering/SkeletalMeshLODModel.h"
|
||||
#include "Rendering/SkeletalMeshModel.h"
|
||||
#include "StaticMeshAttributes.h"
|
||||
#include "SkeletalMeshAttributes.h"
|
||||
#include "StaticToSkeletalMeshConverter.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogLPHairFromStatic, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
T* LoadByPath(const FString& Path)
|
||||
{
|
||||
FString Full = Path;
|
||||
if (!Full.Contains(TEXT(".")))
|
||||
{
|
||||
Full += TEXT(".") + FPackageName::GetShortName(Path);
|
||||
}
|
||||
return LoadObject<T>(nullptr, *Full);
|
||||
}
|
||||
|
||||
/** Sommets du corps rattaches (influence dominante) a l'os HeadIndex ou a un de ses descendants. */
|
||||
TArray<FVector> HeadPoints(const USkeletalMesh* Body, int32 HeadIndex)
|
||||
{
|
||||
TArray<FVector> Points;
|
||||
const FReferenceSkeleton& Ref = Body->GetRefSkeleton();
|
||||
auto IsUnderHead = [&](int32 Bone)
|
||||
{
|
||||
for (; Bone != INDEX_NONE; Bone = Ref.GetParentIndex(Bone))
|
||||
{
|
||||
if (Bone == HeadIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const FSkeletalMeshLODModel& LOD = Body->GetImportedModel()->LODModels[0];
|
||||
for (const FSkelMeshSection& Section : LOD.Sections)
|
||||
{
|
||||
for (const FSoftSkinVertex& V : Section.SoftVertices)
|
||||
{
|
||||
int32 Best = 0;
|
||||
for (int32 i = 1; i < MAX_TOTAL_INFLUENCES; ++i)
|
||||
{
|
||||
if (V.InfluenceWeights[i] > V.InfluenceWeights[Best])
|
||||
{
|
||||
Best = i;
|
||||
}
|
||||
}
|
||||
const int32 Bone = Section.BoneMap.IsValidIndex(V.InfluenceBones[Best]) ? Section.BoneMap[V.InfluenceBones[Best]] : INDEX_NONE;
|
||||
if (IsUnderHead(Bone))
|
||||
{
|
||||
Points.Add(FVector(V.Position));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Points;
|
||||
}
|
||||
|
||||
/** Sommets d'un maillage statique (donnees sources, LOD 0). */
|
||||
TArray<FVector> StaticPoints(UStaticMesh* Mesh)
|
||||
{
|
||||
TArray<FVector> Points;
|
||||
if (const FMeshDescription* Desc = Mesh->GetMeshDescription(0))
|
||||
{
|
||||
TVertexAttributesConstRef<FVector3f> Positions = FStaticMeshConstAttributes(*Desc).GetVertexPositions();
|
||||
for (const FVertexID Vertex : Desc->Vertices().GetElementIDs())
|
||||
{
|
||||
Points.Add(FVector(Positions[Vertex]));
|
||||
}
|
||||
}
|
||||
return Points;
|
||||
}
|
||||
|
||||
/** Boite du haut du crane : sommets situes a moins de DepthFraction x profondeur de la tete sous le sommet. Exclut
|
||||
* oreilles, nez et cou, dont les proportions varient d'une tete de reference a l'autre. */
|
||||
FBox CraniumBox(const TArray<FVector>& Points, double DepthFraction)
|
||||
{
|
||||
FBox Full(Points);
|
||||
const double Floor = Full.Max.Z - DepthFraction * Full.GetSize().Y;
|
||||
FBox Box(ForceInit);
|
||||
for (const FVector& Point : Points)
|
||||
{
|
||||
if (Point.Z >= Floor)
|
||||
{
|
||||
Box += Point;
|
||||
}
|
||||
}
|
||||
return Box;
|
||||
}
|
||||
|
||||
/** Sommet du corps pour le transfert de poids : position, normale, poids exprimes en os du squelette de la coiffure. */
|
||||
struct FBodySample
|
||||
{
|
||||
FVector Position;
|
||||
FVector Normal;
|
||||
TArray<TPair<int32, float>, TInlineAllocator<8>> Weights;
|
||||
};
|
||||
|
||||
/** Os des membres : une meche ne doit pas suivre un bras ni une jambe ; ces os sont ramenes a leur ancetre. */
|
||||
bool IsLimbBone(const FString& Name)
|
||||
{
|
||||
static const TCHAR* Parts[] = { TEXT("Shldr"), TEXT("Forearm"), TEXT("Hand"), TEXT("Carpal"), TEXT("Thumb"), TEXT("Index"),
|
||||
TEXT("Mid"), TEXT("Ring"), TEXT("Pinky"), TEXT("Thigh"), TEXT("Shin"), TEXT("Foot"), TEXT("Toe"), TEXT("Metatarsal") };
|
||||
for (const TCHAR* Part : Parts)
|
||||
{
|
||||
if (Name.Contains(Part))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Sommets du corps au-dessus de MinZ, poids convertis vers les os du squelette cible (ancetre le plus proche present). */
|
||||
TArray<FBodySample> BodySamples(const USkeletalMesh* Body, const FReferenceSkeleton& Target, double MinZ)
|
||||
{
|
||||
const FReferenceSkeleton& Ref = Body->GetRefSkeleton();
|
||||
TArray<int32> BoneMap;
|
||||
BoneMap.SetNum(Ref.GetRawBoneNum());
|
||||
for (int32 Bone = 0; Bone < Ref.GetRawBoneNum(); ++Bone)
|
||||
{
|
||||
int32 Mapped = 0;
|
||||
for (int32 B = Bone; B != INDEX_NONE; B = Ref.GetParentIndex(B))
|
||||
{
|
||||
const FString Name = Ref.GetBoneName(B).ToString();
|
||||
const int32 T = IsLimbBone(Name) ? INDEX_NONE : Target.FindBoneIndex(*Name);
|
||||
if (T != INDEX_NONE)
|
||||
{
|
||||
Mapped = T;
|
||||
break;
|
||||
}
|
||||
}
|
||||
BoneMap[Bone] = Mapped;
|
||||
}
|
||||
TArray<FBodySample> Samples;
|
||||
const FSkeletalMeshLODModel& LOD = Body->GetImportedModel()->LODModels[0];
|
||||
for (const FSkelMeshSection& Section : LOD.Sections)
|
||||
{
|
||||
for (const FSoftSkinVertex& V : Section.SoftVertices)
|
||||
{
|
||||
if (V.Position.Z < MinZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FBodySample& Sample = Samples.AddDefaulted_GetRef();
|
||||
Sample.Position = FVector(V.Position);
|
||||
Sample.Normal = FVector(FVector3f(V.TangentZ)).GetSafeNormal();
|
||||
for (int32 i = 0; i < MAX_TOTAL_INFLUENCES; ++i)
|
||||
{
|
||||
if (V.InfluenceWeights[i] > 0 && Section.BoneMap.IsValidIndex(V.InfluenceBones[i]))
|
||||
{
|
||||
Sample.Weights.Add({ BoneMap[Section.BoneMap[V.InfluenceBones[i]]], V.InfluenceWeights[i] / 65535.f });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Samples;
|
||||
}
|
||||
|
||||
/** Point de reference d'une tete : centre en X et Y, sommet du crane en Z. */
|
||||
FVector TopCenter(const FBox& Box)
|
||||
{
|
||||
return FVector(Box.GetCenter().X, Box.GetCenter().Y, Box.Max.Z);
|
||||
}
|
||||
|
||||
bool SaveAsset(UObject* Asset)
|
||||
{
|
||||
UPackage* Package = Asset->GetOutermost();
|
||||
Package->MarkPackageDirty();
|
||||
const FString Filename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
|
||||
FSavePackageArgs SaveArgs;
|
||||
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
SaveArgs.Error = GWarn;
|
||||
const bool bOk = UPackage::SavePackage(Package, Asset, *Filename, SaveArgs);
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT(" %s %s"), bOk ? TEXT("sauvegarde :") : TEXT("ECHEC de sauvegarde :"), *Filename);
|
||||
return bOk;
|
||||
}
|
||||
}
|
||||
|
||||
ULPHairFromStaticMeshCommandlet::ULPHairFromStaticMeshCommandlet()
|
||||
{
|
||||
IsClient = false;
|
||||
IsEditor = true;
|
||||
IsServer = false;
|
||||
LogToConsole = true;
|
||||
}
|
||||
|
||||
int32 ULPHairFromStaticMeshCommandlet::Main(const FString& Params)
|
||||
{
|
||||
TArray<FString> Tokens, Switches;
|
||||
TMap<FString, FString> P;
|
||||
ParseCommandLine(*Params, Tokens, Switches, P);
|
||||
const FString StaticPath = P.FindRef(TEXT("Static"));
|
||||
const FString FitHeadPath = P.FindRef(TEXT("FitHead"));
|
||||
const FString OutPath = P.FindRef(TEXT("Out"));
|
||||
const FString BodyPath = P.Contains(TEXT("Body")) ? P[TEXT("Body")] : TEXT("/Game/Heavy/Daz/Bridget/bridget_gp");
|
||||
const FString RefHairPath = P.Contains(TEXT("RefHair")) ? P[TEXT("RefHair")] : TEXT("/Game/Heavy/New4/Hairs/FloraHair_New");
|
||||
const FName BoneName(P.Contains(TEXT("Bone")) ? *P[TEXT("Bone")] : TEXT("head"));
|
||||
const double ExtraScale = P.Contains(TEXT("Scale")) ? FCString::Atod(*P[TEXT("Scale")]) : 1.0;
|
||||
const double OffsetZ = P.Contains(TEXT("OffsetZ")) ? FCString::Atod(*P[TEXT("OffsetZ")]) : 0.0;
|
||||
const double OffsetY = P.Contains(TEXT("OffsetY")) ? FCString::Atod(*P[TEXT("OffsetY")]) : 0.0;
|
||||
const double Margin = P.Contains(TEXT("Margin")) ? FCString::Atod(*P[TEXT("Margin")]) : 1.05;
|
||||
const double CraniumDepth = P.Contains(TEXT("Cranium")) ? FCString::Atod(*P[TEXT("Cranium")]) : 0.3;
|
||||
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
|
||||
// Liaison : "transfer" (defaut) = poids copies des sommets du corps les plus proches, sommets sous la peau repousses ;
|
||||
// "rigid" = tout sur l'os -Bone (premiere version ; les meches longues traversent epaules et poitrine quand la tete
|
||||
// tourne).
|
||||
const bool bRigid = P.FindRef(TEXT("Skin")).Equals(TEXT("rigid"), ESearchCase::IgnoreCase);
|
||||
const double Gap = P.Contains(TEXT("Gap")) ? FCString::Atod(*P[TEXT("Gap")]) : 0.3;
|
||||
const int32 Neighbors = P.Contains(TEXT("Neighbors")) ? FCString::Atoi(*P[TEXT("Neighbors")]) : 6;
|
||||
if (StaticPath.IsEmpty() || FitHeadPath.IsEmpty() || OutPath.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Usage : -Static=<StaticMesh> -FitHead=<StaticMesh> -Out=</Game/...> [-Body=] [-RefHair=] [-Bone=head] [-Scale=] [-OffsetZ=] [-OffsetY=] [-DryRun]"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
UStaticMesh* Static = LoadByPath<UStaticMesh>(StaticPath);
|
||||
UStaticMesh* FitHead = LoadByPath<UStaticMesh>(FitHeadPath);
|
||||
USkeletalMesh* Body = LoadByPath<USkeletalMesh>(BodyPath);
|
||||
USkeletalMesh* RefHair = LoadByPath<USkeletalMesh>(RefHairPath);
|
||||
if (!Static || !FitHead || !Body || !RefHair || !RefHair->GetSkeleton())
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Asset introuvable : Static=%d FitHead=%d Body=%d RefHair=%d"), !!Static, !!FitHead, !!Body, RefHair && RefHair->GetSkeleton());
|
||||
return 1;
|
||||
}
|
||||
if (FPackageName::DoesPackageExist(OutPath))
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("%s existe deja : rien n'est ecrase"), *OutPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 1. Mesures.
|
||||
const int32 BodyHead = Body->GetRefSkeleton().FindBoneIndex(BoneName);
|
||||
const FReferenceSkeleton& HairRef = RefHair->GetSkeleton()->GetReferenceSkeleton();
|
||||
const int32 HairHead = HairRef.FindBoneIndex(BoneName);
|
||||
if (BodyHead == INDEX_NONE || HairHead == INDEX_NONE)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Os '%s' absent du corps (%d) ou du squelette de reference (%d)"), *BoneName.ToString(), BodyHead, HairHead);
|
||||
return 1;
|
||||
}
|
||||
const TArray<FVector> BodyPointsList = HeadPoints(Body, BodyHead);
|
||||
const TArray<FVector> FitPointsList = StaticPoints(FitHead);
|
||||
const int32 HeadVerts = BodyPointsList.Num();
|
||||
if (HeadVerts == 0 || FitPointsList.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Mesure impossible : %d sommets de tete sur le corps, %d sur la tete de reference"), HeadVerts, FitPointsList.Num());
|
||||
return 1;
|
||||
}
|
||||
// Ajustement sur le haut du crane (largeur, profondeur, centre) et le sommet : la boite complete inclut oreilles,
|
||||
// nez et cou, et donnait une coiffure trop etroite enfoncee dans le crane (premier essai O7e).
|
||||
const FBox BodyFull(BodyPointsList);
|
||||
const FBox FitFull(FitPointsList);
|
||||
const FBox BodyBox = CraniumBox(BodyPointsList, CraniumDepth);
|
||||
const FBox FitBox = CraniumBox(FitPointsList, CraniumDepth);
|
||||
// Echelle par axe (largeur X, profondeur Y), hauteur : moyenne des deux ; -Uniform : echelle unique (largeur).
|
||||
// Marge : la calotte reste au-dessus du cuir chevelu.
|
||||
const double ScaleX = BodyBox.GetSize().X / FitBox.GetSize().X;
|
||||
const double ScaleY = Switches.Contains(TEXT("Uniform")) ? ScaleX : BodyBox.GetSize().Y / FitBox.GetSize().Y;
|
||||
const FVector Scale = FVector(ScaleX, ScaleY, (ScaleX + ScaleY) * 0.5) * Margin * ExtraScale;
|
||||
const FVector From = TopCenter(FitBox);
|
||||
const FVector To = TopCenter(BodyBox) + FVector(0.0, OffsetY, OffsetZ);
|
||||
const FTransform BodyHeadCS = FAnimationRuntime::GetComponentSpaceTransformRefPose(Body->GetRefSkeleton(), BodyHead);
|
||||
const FTransform HairHeadCS = FAnimationRuntime::GetComponentSpaceTransformRefPose(HairRef, HairHead);
|
||||
const FTransform Correction = BodyHeadCS.Inverse() * HairHeadCS;
|
||||
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Tete du corps (%d sommets) : taille %s ; haut du crane %s, taille %s"), HeadVerts, *BodyFull.GetSize().ToString(), *BodyBox.ToString(), *BodyBox.GetSize().ToString());
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Tete de reference : taille %s ; haut du crane %s, taille %s"), *FitFull.GetSize().ToString(), *FitBox.ToString(), *FitBox.GetSize().ToString());
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Ajustement : echelle %s, %s -> %s"), *Scale.ToString(), *From.ToString(), *To.ToString());
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Os '%s' : corps %s, squelette cible %s, correction %s"), *BoneName.ToString(),
|
||||
*BodyHeadCS.GetLocation().ToString(), *HairHeadCS.GetLocation().ToString(), Correction.Equals(FTransform::Identity, 0.01) ? TEXT("aucune") : *Correction.ToString());
|
||||
|
||||
// 2. Copie de travail de la coiffure, sommets places sur la tete du corps.
|
||||
UStaticMesh* Work = DuplicateObject<UStaticMesh>(Static, GetTransientPackage(), NAME_None);
|
||||
FMeshDescription* Desc = Work ? Work->GetMeshDescription(0) : nullptr;
|
||||
if (!Desc)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Donnees sources de %s indisponibles"), *StaticPath);
|
||||
return 1;
|
||||
}
|
||||
FBox Placed(ForceInit);
|
||||
TVertexAttributesRef<FVector3f> Positions = FStaticMeshAttributes(*Desc).GetVertexPositions();
|
||||
for (const FVertexID Vertex : Desc->Vertices().GetElementIDs())
|
||||
{
|
||||
const FVector OnBody = (FVector(Positions[Vertex]) - From) * Scale + To;
|
||||
const FVector Final = Correction.TransformPosition(OnBody);
|
||||
Positions[Vertex] = FVector3f(Final);
|
||||
Placed += Final;
|
||||
}
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Coiffure placee : %s (avant : %s)"), *Placed.ToString(), *Static->GetBoundingBox().ToString());
|
||||
|
||||
// 2b. Transfert de poids depuis le corps et sortie de la peau.
|
||||
TMap<FVertexID, TArray<UE::AnimationCore::FBoneWeight>> VertexWeights;
|
||||
if (!bRigid)
|
||||
{
|
||||
const TArray<FBodySample> Samples = BodySamples(Body, HairRef, Placed.Min.Z - 15.0);
|
||||
int32 Pushed = 0;
|
||||
TMap<int32, double> BoneShare;
|
||||
for (const FVertexID Vertex : Desc->Vertices().GetElementIDs())
|
||||
{
|
||||
FVector Position(Positions[Vertex]);
|
||||
// k plus proches sommets du corps (recherche exhaustive : quelques milliers x quelques dizaines de milliers).
|
||||
TArray<TPair<double, int32>, TInlineAllocator<16>> Nearest;
|
||||
for (int32 i = 0; i < Samples.Num(); ++i)
|
||||
{
|
||||
const double D2 = FVector::DistSquared(Position, Samples[i].Position);
|
||||
if (Nearest.Num() < Neighbors || D2 < Nearest.Last().Key)
|
||||
{
|
||||
int32 At = Nearest.Num();
|
||||
while (At > 0 && Nearest[At - 1].Key > D2) { --At; }
|
||||
Nearest.Insert(TPair<double, int32>(D2, i), At);
|
||||
if (Nearest.Num() > Neighbors) { Nearest.Pop(EAllowShrinking::No); }
|
||||
}
|
||||
}
|
||||
if (Nearest.Num() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Sortie de la peau : un sommet a moins de Gap devant la surface (ou derriere) est repousse le long de la normale.
|
||||
const FBodySample& Closest = Samples[Nearest[0].Value];
|
||||
const double Depth = FVector::DotProduct(Position - Closest.Position, Closest.Normal);
|
||||
if (Depth < Gap && FMath::Sqrt(Nearest[0].Key) < 4.0)
|
||||
{
|
||||
Position += Closest.Normal * (Gap - Depth);
|
||||
Positions[Vertex] = FVector3f(Position);
|
||||
++Pushed;
|
||||
}
|
||||
// Poids : moyenne des voisins ponderee par l'inverse de la distance, 4 influences au plus.
|
||||
TMap<int32, float> Accum;
|
||||
for (const TPair<double, int32>& N : Nearest)
|
||||
{
|
||||
const float W = 1.f / (float(FMath::Sqrt(N.Key)) + 0.5f);
|
||||
for (const TPair<int32, float>& BW : Samples[N.Value].Weights)
|
||||
{
|
||||
Accum.FindOrAdd(BW.Key) += BW.Value * W;
|
||||
}
|
||||
}
|
||||
Accum.ValueSort([](float A, float B) { return A > B; });
|
||||
// Normalisation avant creation des poids : FBoneWeight borne sa valeur a 1.
|
||||
TArray<TPair<int32, float>, TInlineAllocator<4>> Top;
|
||||
float Total = 0.f;
|
||||
for (const TPair<int32, float>& A : Accum)
|
||||
{
|
||||
if (Top.Num() == 4) { break; }
|
||||
Top.Add(A);
|
||||
Total += A.Value;
|
||||
}
|
||||
TArray<UE::AnimationCore::FBoneWeight> Weights;
|
||||
for (const TPair<int32, float>& T : Top)
|
||||
{
|
||||
Weights.Add(UE::AnimationCore::FBoneWeight(FBoneIndexType(T.Key), T.Value / Total));
|
||||
BoneShare.FindOrAdd(T.Key) += T.Value / Total;
|
||||
}
|
||||
VertexWeights.Add(Vertex, MoveTemp(Weights));
|
||||
}
|
||||
BoneShare.ValueSort([](double A, double B) { return A > B; });
|
||||
FString Shares;
|
||||
for (const TPair<int32, double>& S : BoneShare)
|
||||
{
|
||||
Shares += FString::Printf(TEXT("%s %.0f%%, "), *HairRef.GetBoneName(S.Key).ToString(), 100.0 * S.Value / FMath::Max(1, VertexWeights.Num()));
|
||||
}
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Transfert de poids : %d sommets du corps, %d sommets de coiffure, %d repousse(s) hors de la peau (%.1f mm) ; os : %s"),
|
||||
Samples.Num(), VertexWeights.Num(), Pushed, Gap * 10.0, *Shares.LeftChop(2));
|
||||
}
|
||||
if (bDryRun)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("-DryRun : rien n'est cree"));
|
||||
return 0;
|
||||
}
|
||||
if (bRigid)
|
||||
{
|
||||
Work->CommitMeshDescription(0);
|
||||
Work->Build(true);
|
||||
}
|
||||
|
||||
// 3. Squelette (copie de celui de la coiffure de reference) et SkeletalMesh.
|
||||
const FString OutName = FPackageName::GetShortName(OutPath);
|
||||
UPackage* SkeletonPackage = CreatePackage(*(OutPath + TEXT("_Skeleton")));
|
||||
USkeleton* Skeleton = DuplicateObject<USkeleton>(RefHair->GetSkeleton(), SkeletonPackage, *(OutName + TEXT("_Skeleton")));
|
||||
Skeleton->SetFlags(RF_Public | RF_Standalone);
|
||||
Skeleton->SetPreviewMesh(nullptr);
|
||||
|
||||
UPackage* MeshPackage = CreatePackage(*OutPath);
|
||||
USkeletalMesh* Hair = NewObject<USkeletalMesh>(MeshPackage, *OutName, RF_Public | RF_Standalone);
|
||||
bool bConverted = false;
|
||||
if (bRigid)
|
||||
{
|
||||
bConverted = FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromStaticMesh(Hair, Work, Skeleton->GetReferenceSkeleton(), BoneName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Description du maillage (positions placees et repoussees) + poids de peau ; materiaux de la coiffure statique.
|
||||
FMeshDescription SkinnedDesc(*Desc);
|
||||
FSkeletalMeshAttributes Attributes(SkinnedDesc);
|
||||
Attributes.Register(true);
|
||||
FSkinWeightsVertexAttributesRef SkinWeights = Attributes.GetVertexSkinWeights();
|
||||
const int32 HeadIndex = HairRef.FindBoneIndex(BoneName);
|
||||
for (const FVertexID Vertex : SkinnedDesc.Vertices().GetElementIDs())
|
||||
{
|
||||
const TArray<UE::AnimationCore::FBoneWeight>* Weights = VertexWeights.Find(Vertex);
|
||||
if (Weights && Weights->Num() > 0)
|
||||
{
|
||||
SkinWeights.Set(Vertex, *Weights);
|
||||
}
|
||||
else
|
||||
{
|
||||
const UE::AnimationCore::FBoneWeight HeadOnly(FBoneIndexType(HeadIndex), 1.0f);
|
||||
SkinWeights.Set(Vertex, MakeArrayView(&HeadOnly, 1));
|
||||
}
|
||||
}
|
||||
TArray<FSkeletalMaterial> Materials;
|
||||
for (const FStaticMaterial& M : Static->GetStaticMaterials())
|
||||
{
|
||||
Materials.Add(FSkeletalMaterial(M.MaterialInterface, M.MaterialSlotName, M.ImportedMaterialSlotName));
|
||||
}
|
||||
const FMeshDescription* Descs[] = { &SkinnedDesc };
|
||||
bConverted = FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromMeshDescriptions(Hair, Descs, Materials, Skeleton->GetReferenceSkeleton(), false, true);
|
||||
}
|
||||
if (!bConverted)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Conversion en SkeletalMesh refusee par le moteur"));
|
||||
return 1;
|
||||
}
|
||||
Hair->SetSkeleton(Skeleton);
|
||||
Skeleton->MergeAllBonesToBoneTree(Hair);
|
||||
Skeleton->SetPreviewMesh(Hair);
|
||||
|
||||
// 4. Controle puis sauvegarde.
|
||||
const int32 Bones = Hair->GetRefSkeleton().GetNum();
|
||||
const FBox Bounds = Hair->GetImportedBounds().GetBox();
|
||||
UE_LOG(LogLPHairFromStatic, Warning, TEXT("%s : %d os, %d materiau(x), bornes %s"), *OutName, Bones, Hair->GetMaterials().Num(), *Bounds.ToString());
|
||||
if (Bones != HairRef.GetNum() || Hair->GetRefSkeleton().FindBoneIndex(BoneName) == INDEX_NONE)
|
||||
{
|
||||
UE_LOG(LogLPHairFromStatic, Error, TEXT("Squelette inattendu : rien n'est sauvegarde"));
|
||||
return 1;
|
||||
}
|
||||
return SaveAsset(Skeleton) && SaveAsset(Hair) ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// Portage UE 5.5 - lot O7e (Docs/Migration/11_Audit_Optimisation.md §16.8).
|
||||
//
|
||||
// Les coiffures LifePlay sont des SkeletalMesh dont les os portent les noms Genesis 8 : Person les fait suivre le corps
|
||||
// par SetLeaderPoseComponent (correspondance par nom d'os). Une coiffure de Fab est le plus souvent un maillage statique,
|
||||
// modele autour d'une tete de reference qui n'est pas celle de Bridget.
|
||||
//
|
||||
// Cette commandlet :
|
||||
// 1. mesure la tete du corps (-Body, sommets rattaches a l'os -Bone ou a ses descendants) et la tete de reference
|
||||
// fournie avec la coiffure (-FitHead) ;
|
||||
// 2. place la coiffure (-Static) sur la tete du corps : echelle par axe mesuree sur le haut du crane, sans
|
||||
// oreilles, nez ni cou (largeur, profondeur ; -Uniform : largeur seule), alignement du centre et du sommet du crane, puis correction si l'os de liaison du squelette cible n'a pas la meme pose de reference que
|
||||
// celui du corps ;
|
||||
// 3. cree un squelette (copie de celui de -RefHair, une coiffure existante) et le SkeletalMesh -Out, tous les sommets
|
||||
// lies rigidement a l'os -Bone (FStaticToSkeletalMeshConverter du moteur) ;
|
||||
// 4. sauvegarde. Aucun asset existant n'est modifie (les sources sont dupliquees en memoire).
|
||||
//
|
||||
// Usage :
|
||||
// UnrealEditor-Cmd.exe <projet> -run=LPHairFromStaticMesh -Static=/Game/.../Hair_17 -FitHead=/Game/.../Head
|
||||
// -Out=/Game/Heavy/New5/Hairs/Pocolov17 [-Body=/Game/Heavy/Daz/Bridget/bridget_gp]
|
||||
// [-RefHair=/Game/Heavy/New4/Hairs/FloraHair_New] [-Bone=head] [-Scale=1.0] [-OffsetZ=0] [-OffsetY=0] [-Margin=1.05] [-Cranium=0.3]
|
||||
// [-Uniform] [-DryRun]
|
||||
// -Scale, -OffsetZ, -OffsetY : retouches manuelles appliquees apres l'ajustement automatique (cm).
|
||||
// -Cranium : epaisseur mesuree sous le sommet du crane, en fraction de la profondeur de la tete (defaut 0,3) ;
|
||||
// -Margin : agrandissement de securite de la calotte (defaut 1,05).
|
||||
// -Skin=transfer (defaut) : chaque sommet recoit les poids des -Neighbors (6) sommets du corps les plus proches
|
||||
// (bras et jambes exclus) et tout sommet a moins de -Gap (0,3 cm) de la peau, ou dessous, est repousse le long de la
|
||||
// normale du corps : les meches longues suivent cou, epaules et poitrine. -Skin=rigid : tout sur l'os -Bone.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "LPHairFromStaticMeshCommandlet.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class ULPHairFromStaticMeshCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULPHairFromStaticMeshCommandlet();
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
|
|
@ -17,9 +17,9 @@ public class LifePlayEditorTools : ModuleRules
|
|||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"LifePlay",
|
||||
// Lot S2 : LPAnimChecks.h expose FLPPose (LPPoseMeasure.h) dans sa signature.
|
||||
"LPAnimMeasure"
|
||||
"LifePlay"
|
||||
// Lot DC2 : les commandlets de mods (LPAnim*, LPHairFromStaticMesh) et leur dependance a
|
||||
// LPAnimMeasure sont dans le module LPModTools de l'addon LifePlay-Sandbox.
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
|
|
@ -40,16 +40,8 @@ public class LifePlayEditorTools : ModuleRules
|
|||
// UWidgetBlueprint au meme titre que les UBlueprint
|
||||
"UMG",
|
||||
"UMGEditor",
|
||||
// Lot AP2 : LPAnimEvalCommandlet ecrit ses rapports en JSON
|
||||
"Json",
|
||||
// Portage 5.5 (L7h) : LPCheckMeshesCommandlet lit les donnees sources des UStaticMesh
|
||||
"MeshDescription",
|
||||
// O7e : LPHairFromStaticMeshCommandlet (conversion maillage statique -> SkeletalMesh)
|
||||
"StaticMeshDescription",
|
||||
"SkeletalMeshUtilitiesCommon",
|
||||
// Transfert de poids (FSkeletalMeshAttributes, FBoneWeight)
|
||||
"SkeletalMeshDescription",
|
||||
"AnimationCore",
|
||||
// UI14 : LPEditWidgetCommandlet lit sa specification JSON
|
||||
"Json"
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue