// 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(FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Trunk.GetSafeNormal().Z, -1.0, 1.0)))); if (Tilt > LyingTilt) { return TEXT("allonge"); } const float Hips = static_cast(At(P, B.Hips).Z) - Floor; if (Hips >= LowHips * StandingHips) { return TEXT("debout"); } const float Knee = static_cast(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(FVector::DotProduct(Flat(Ankles - At(P, B.Hips)), ForwardOf(P, B))); return Behind > SeatBehind ? TEXT("assis") : TEXT("accroupi"); } bool ReadJson(const FString& Path, TSharedPtr& Out) { FString Text; if (!FFileHelper::LoadFileToString(Text, *Path)) { return false; } const TSharedRef> 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(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(Model->GetPlayLength()); return Out.Frames > 0; } TSharedRef Measure(const FString& Path, const FClipInfo& Info, FString& Error) { TSharedRef Card = MakeShared(); TArray Poses; Poses.SetNum(Info.Frames); for (int32 Key = 0; Key < Info.Frames; ++Key) { const float Time = FMath::Min(static_cast(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(FMath::Min(RestAt(First, B.FootL).Z, RestAt(First, B.FootR).Z)); const float StandingHips = static_cast(RestAt(First, B.Hips).Z) - Floor; const float Height = FMath::Max(1.f, static_cast(RestAt(First, B.Head).Z) - Floor); Card->SetNumberField(TEXT("images"), Info.Frames); Card->SetNumberField(TEXT("cadence"), FMath::RoundToFloat(static_cast(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((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(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(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 Count; TArray SeatHeights; int32 FeetDown[2] = { 0, 0 }; int32 Raised[2] = { 0, 0 }; float HandPath[2] = { 0.f, 0.f }; const float RestFoot[2] = { static_cast(RestAt(First, B.FootL).Z), static_cast(RestAt(First, B.FootR).Z) }; const float RestToe[2] = { static_cast(RestAt(First, B.ToeL).Z), static_cast(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((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(At(P, Feet[Side]).Z) - RestFoot[Side], static_cast(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(((At(P, Hands[Side]) - At(P, B.Hips)) - (At(Q, Hands[Side]) - At(Q, B.Hips))).Size()); } } } FString Main = TEXT("variable"); for (const TPair& 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 Split = MakeShared(); for (const TPair& 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()); } const TCHAR* SideNames[2] = { TEXT("gauche"), TEXT("droite") }; TSharedRef Feet = MakeShared(); TSharedRef Hands = MakeShared(); 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 Root; if (!ReadJson(Livraison, Root) || Root->Type != EJson::Array) { UE_LOG(LogLPAnimCatalog, Error, TEXT("livraison.json illisible : %s"), *Livraison); return 1; } TSharedRef Cards = MakeShared(); TArray Failures; for (const TSharedPtr& Entry : Root->AsArray()) { const TSharedPtr 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 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 Report = MakeShared(); 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> Errors; for (const FString& Failure : Failures) { Errors.Add(MakeShared(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> 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; }