LifePlay-Revival/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.cpp

227 lines
9.5 KiB
C++
Raw Normal View History

// Portage UE 5.5 - voir LPHairFromStaticMeshCommandlet.h
#include "LPHairFromStaticMeshCommandlet.h"
#include "AnimationRuntime.h"
#include "Animation/Skeleton.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/StaticMesh.h"
#include "MeshDescription.h"
#include "Misc/PackageName.h"
#include "Rendering/SkeletalMeshLODModel.h"
#include "Rendering/SkeletalMeshModel.h"
#include "StaticMeshAttributes.h"
#include "StaticToSkeletalMeshConverter.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPHairFromStatic, Log, All);
namespace
{
template <typename T>
T* LoadByPath(const FString& Path)
{
FString Full = Path;
if (!Full.Contains(TEXT(".")))
{
Full += TEXT(".") + FPackageName::GetShortName(Path);
}
return LoadObject<T>(nullptr, *Full);
}
/** Boite des sommets du corps rattaches (influence dominante) a l'os HeadIndex ou a un de ses descendants. */
FBox HeadBox(const USkeletalMesh* Body, int32 HeadIndex, int32& OutCount)
{
FBox Box(ForceInit);
OutCount = 0;
const FReferenceSkeleton& Ref = Body->GetRefSkeleton();
auto IsUnderHead = [&](int32 Bone)
{
for (; Bone != INDEX_NONE; Bone = Ref.GetParentIndex(Bone))
{
if (Bone == HeadIndex)
{
return true;
}
}
return false;
};
const FSkeletalMeshLODModel& LOD = Body->GetImportedModel()->LODModels[0];
for (const FSkelMeshSection& Section : LOD.Sections)
{
for (const FSoftSkinVertex& V : Section.SoftVertices)
{
int32 Best = 0;
for (int32 i = 1; i < MAX_TOTAL_INFLUENCES; ++i)
{
if (V.InfluenceWeights[i] > V.InfluenceWeights[Best])
{
Best = i;
}
}
const int32 Bone = Section.BoneMap.IsValidIndex(V.InfluenceBones[Best]) ? Section.BoneMap[V.InfluenceBones[Best]] : INDEX_NONE;
if (IsUnderHead(Bone))
{
Box += FVector(V.Position);
++OutCount;
}
}
}
return Box;
}
/** Point de reference d'une tete : centre en X et Y, sommet du crane en Z. */
FVector TopCenter(const FBox& Box)
{
return FVector(Box.GetCenter().X, Box.GetCenter().Y, Box.Max.Z);
}
bool SaveAsset(UObject* Asset)
{
UPackage* Package = Asset->GetOutermost();
Package->MarkPackageDirty();
const FString Filename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.Error = GWarn;
const bool bOk = UPackage::SavePackage(Package, Asset, *Filename, SaveArgs);
UE_LOG(LogLPHairFromStatic, Warning, TEXT(" %s %s"), bOk ? TEXT("sauvegarde :") : TEXT("ECHEC de sauvegarde :"), *Filename);
return bOk;
}
}
ULPHairFromStaticMeshCommandlet::ULPHairFromStaticMeshCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 ULPHairFromStaticMeshCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens, Switches;
TMap<FString, FString> P;
ParseCommandLine(*Params, Tokens, Switches, P);
const FString StaticPath = P.FindRef(TEXT("Static"));
const FString FitHeadPath = P.FindRef(TEXT("FitHead"));
const FString OutPath = P.FindRef(TEXT("Out"));
const FString BodyPath = P.Contains(TEXT("Body")) ? P[TEXT("Body")] : TEXT("/Game/Heavy/Daz/Bridget/bridget_gp");
const FString RefHairPath = P.Contains(TEXT("RefHair")) ? P[TEXT("RefHair")] : TEXT("/Game/Heavy/New4/Hairs/FloraHair_New");
const FName BoneName(P.Contains(TEXT("Bone")) ? *P[TEXT("Bone")] : TEXT("head"));
const double ExtraScale = P.Contains(TEXT("Scale")) ? FCString::Atod(*P[TEXT("Scale")]) : 1.0;
const double OffsetZ = P.Contains(TEXT("OffsetZ")) ? FCString::Atod(*P[TEXT("OffsetZ")]) : 0.0;
const double OffsetY = P.Contains(TEXT("OffsetY")) ? FCString::Atod(*P[TEXT("OffsetY")]) : 0.0;
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
if (StaticPath.IsEmpty() || FitHeadPath.IsEmpty() || OutPath.IsEmpty())
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Usage : -Static=<StaticMesh> -FitHead=<StaticMesh> -Out=</Game/...> [-Body=] [-RefHair=] [-Bone=head] [-Scale=] [-OffsetZ=] [-OffsetY=] [-DryRun]"));
return 1;
}
UStaticMesh* Static = LoadByPath<UStaticMesh>(StaticPath);
UStaticMesh* FitHead = LoadByPath<UStaticMesh>(FitHeadPath);
USkeletalMesh* Body = LoadByPath<USkeletalMesh>(BodyPath);
USkeletalMesh* RefHair = LoadByPath<USkeletalMesh>(RefHairPath);
if (!Static || !FitHead || !Body || !RefHair || !RefHair->GetSkeleton())
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Asset introuvable : Static=%d FitHead=%d Body=%d RefHair=%d"), !!Static, !!FitHead, !!Body, RefHair && RefHair->GetSkeleton());
return 1;
}
if (FPackageName::DoesPackageExist(OutPath))
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("%s existe deja : rien n'est ecrase"), *OutPath);
return 1;
}
// 1. Mesures.
const int32 BodyHead = Body->GetRefSkeleton().FindBoneIndex(BoneName);
const FReferenceSkeleton& HairRef = RefHair->GetSkeleton()->GetReferenceSkeleton();
const int32 HairHead = HairRef.FindBoneIndex(BoneName);
if (BodyHead == INDEX_NONE || HairHead == INDEX_NONE)
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Os '%s' absent du corps (%d) ou du squelette de reference (%d)"), *BoneName.ToString(), BodyHead, HairHead);
return 1;
}
int32 HeadVerts = 0;
const FBox BodyBox = HeadBox(Body, BodyHead, HeadVerts);
const FBox FitBox = FitHead->GetBoundingBox();
if (HeadVerts == 0 || FitBox.GetSize().X <= 0.0)
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Mesure impossible : %d sommets de tete sur le corps, largeur de la tete de reference %.2f"), HeadVerts, FitBox.GetSize().X);
return 1;
}
// Echelle par axe : largeur (X) et profondeur (Y) des deux tetes ; hauteur : moyenne des deux (la boite de la tete de
// reference inclut le cou, sa hauteur n'est pas comparable). -Uniform : echelle unique calee sur la largeur.
const double ScaleX = BodyBox.GetSize().X / FitBox.GetSize().X;
const double ScaleY = Switches.Contains(TEXT("Uniform")) ? ScaleX : BodyBox.GetSize().Y / FitBox.GetSize().Y;
const FVector Scale = FVector(ScaleX, ScaleY, (ScaleX + ScaleY) * 0.5) * ExtraScale;
const FVector From = TopCenter(FitBox);
const FVector To = TopCenter(BodyBox) + FVector(0.0, OffsetY, OffsetZ);
const FTransform BodyHeadCS = FAnimationRuntime::GetComponentSpaceTransformRefPose(Body->GetRefSkeleton(), BodyHead);
const FTransform HairHeadCS = FAnimationRuntime::GetComponentSpaceTransformRefPose(HairRef, HairHead);
const FTransform Correction = BodyHeadCS.Inverse() * HairHeadCS;
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Tete du corps (%d sommets) : %s, taille %s"), HeadVerts, *BodyBox.ToString(), *BodyBox.GetSize().ToString());
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Tete de reference : %s, taille %s"), *FitBox.ToString(), *FitBox.GetSize().ToString());
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Ajustement : echelle %s, %s -> %s"), *Scale.ToString(), *From.ToString(), *To.ToString());
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Os '%s' : corps %s, squelette cible %s, correction %s"), *BoneName.ToString(),
*BodyHeadCS.GetLocation().ToString(), *HairHeadCS.GetLocation().ToString(), Correction.Equals(FTransform::Identity, 0.01) ? TEXT("aucune") : *Correction.ToString());
// 2. Copie de travail de la coiffure, sommets places sur la tete du corps.
UStaticMesh* Work = DuplicateObject<UStaticMesh>(Static, GetTransientPackage(), NAME_None);
FMeshDescription* Desc = Work ? Work->GetMeshDescription(0) : nullptr;
if (!Desc)
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Donnees sources de %s indisponibles"), *StaticPath);
return 1;
}
FBox Placed(ForceInit);
TVertexAttributesRef<FVector3f> Positions = FStaticMeshAttributes(*Desc).GetVertexPositions();
for (const FVertexID Vertex : Desc->Vertices().GetElementIDs())
{
const FVector OnBody = (FVector(Positions[Vertex]) - From) * Scale + To;
const FVector Final = Correction.TransformPosition(OnBody);
Positions[Vertex] = FVector3f(Final);
Placed += Final;
}
UE_LOG(LogLPHairFromStatic, Warning, TEXT("Coiffure placee : %s (avant : %s)"), *Placed.ToString(), *Static->GetBoundingBox().ToString());
if (bDryRun)
{
UE_LOG(LogLPHairFromStatic, Warning, TEXT("-DryRun : rien n'est cree"));
return 0;
}
Work->CommitMeshDescription(0);
Work->Build(true);
// 3. Squelette (copie de celui de la coiffure de reference) et SkeletalMesh.
const FString OutName = FPackageName::GetShortName(OutPath);
UPackage* SkeletonPackage = CreatePackage(*(OutPath + TEXT("_Skeleton")));
USkeleton* Skeleton = DuplicateObject<USkeleton>(RefHair->GetSkeleton(), SkeletonPackage, *(OutName + TEXT("_Skeleton")));
Skeleton->SetFlags(RF_Public | RF_Standalone);
Skeleton->SetPreviewMesh(nullptr);
UPackage* MeshPackage = CreatePackage(*OutPath);
USkeletalMesh* Hair = NewObject<USkeletalMesh>(MeshPackage, *OutName, RF_Public | RF_Standalone);
if (!FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromStaticMesh(Hair, Work, Skeleton->GetReferenceSkeleton(), BoneName))
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Conversion en SkeletalMesh refusee par le moteur"));
return 1;
}
Hair->SetSkeleton(Skeleton);
Skeleton->MergeAllBonesToBoneTree(Hair);
Skeleton->SetPreviewMesh(Hair);
// 4. Controle puis sauvegarde.
const int32 Bones = Hair->GetRefSkeleton().GetNum();
const FBox Bounds = Hair->GetImportedBounds().GetBox();
UE_LOG(LogLPHairFromStatic, Warning, TEXT("%s : %d os, %d materiau(x), bornes %s"), *OutName, Bones, Hair->GetMaterials().Num(), *Bounds.ToString());
if (Bones != HairRef.GetNum() || Hair->GetRefSkeleton().FindBoneIndex(BoneName) == INDEX_NONE)
{
UE_LOG(LogLPHairFromStatic, Error, TEXT("Squelette inattendu : rien n'est sauvegarde"));
return 1;
}
return SaveAsset(Skeleton) && SaveAsset(Hair) ? 0 : 1;
}