Charge le plugin via AdditionalPluginDirectories, conserve ShowCouple et les commandlets anim côté jeu, et documente les lots S0–S8 (CHG-212 à CHG-220). Co-authored-by: Cursor <cursoragent@cursor.com>
348 lines
13 KiB
C++
348 lines
13 KiB
C++
// 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;
|
|
}
|