216 lines
7.6 KiB
C++
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;
|
|
}
|