LifePlay-Revival/Source/LifePlayEditorTools/LPAnimIdentifyCommandlet.cpp
2026-10-07 15:49:56 +02:00

216 lines
7.6 KiB
C++

// 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;
}