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>
459 lines
19 KiB
C++
459 lines
19 KiB
C++
#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 "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;
|
|
}
|
|
|
|
// Clips de reference du dossier qui ont une copie pour chaque corps (prefixe du corps + nom).
|
|
TArray<FString> ListClips(const FString& Dir, const TArray<FBody>& Bodies)
|
|
{
|
|
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
|
Registry.ScanPathsSynchronous({ Dir }, true);
|
|
TArray<FAssetData> Assets;
|
|
Registry.GetAssetsByPath(FName(*Dir), Assets, false);
|
|
TSet<FString> Names;
|
|
for (const FAssetData& Asset : Assets)
|
|
{
|
|
if (Asset.AssetClassPath == UAnimSequence::StaticClass()->GetClassPathName())
|
|
{
|
|
Names.Add(Asset.AssetName.ToString());
|
|
}
|
|
}
|
|
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 &= Names.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, 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;
|
|
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));
|
|
if (Adjust.Degrees > SeuilReglage)
|
|
{
|
|
Failures.Add(FString::Printf(TEXT("%s : reglage %s %.1f deg (borne %.1f)"), *Body.Name, *Adjust.Bone.ToString(), Adjust.Degrees, SeuilReglage));
|
|
}
|
|
}
|
|
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;
|
|
int32 Instants = 3;
|
|
float SeuilPoseT = 1.f;
|
|
float SeuilSymetrie = 0.5f;
|
|
float SeuilReglage = 30.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("SeuilEval="), SeuilEval);
|
|
// 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->SetNumberField(TEXT("instants"), Instants);
|
|
|
|
// 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(ClipPath(ClipDir, Body.Prefix + RefClip), 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, Failures);
|
|
}
|
|
|
|
// Etape 5 : formule et clip converti contre la reference, a chaque instant.
|
|
TArray<FString> Clips = ListClips(ClipDir, 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(ClipDir, 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(ClipPath(ClipDir, Body.Prefix + RefClip), 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(ClipDir, 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;
|
|
}
|