2026-10-02 13:24:54 +00:00
// 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 ) ;
}
2026-10-02 13:35:23 +00:00
/** Sommets du corps rattaches (influence dominante) a l'os HeadIndex ou a un de ses descendants. */
TArray < FVector > HeadPoints ( const USkeletalMesh * Body , int32 HeadIndex )
2026-10-02 13:24:54 +00:00
{
2026-10-02 13:35:23 +00:00
TArray < FVector > Points ;
2026-10-02 13:24:54 +00:00
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 ) )
{
2026-10-02 13:35:23 +00:00
Points . Add ( FVector ( V . Position ) ) ;
2026-10-02 13:24:54 +00:00
}
}
}
2026-10-02 13:35:23 +00:00
return Points ;
}
/** Sommets d'un maillage statique (donnees sources, LOD 0). */
TArray < FVector > StaticPoints ( UStaticMesh * Mesh )
{
TArray < FVector > Points ;
if ( const FMeshDescription * Desc = Mesh - > GetMeshDescription ( 0 ) )
{
TVertexAttributesConstRef < FVector3f > Positions = FStaticMeshConstAttributes ( * Desc ) . GetVertexPositions ( ) ;
for ( const FVertexID Vertex : Desc - > Vertices ( ) . GetElementIDs ( ) )
{
Points . Add ( FVector ( Positions [ Vertex ] ) ) ;
}
}
return Points ;
}
/** Boite du haut du crane : sommets situes a moins de DepthFraction x profondeur de la tete sous le sommet. Exclut
* oreilles , nez et cou , dont les proportions varient d ' une tete de reference a l ' autre . */
FBox CraniumBox ( const TArray < FVector > & Points , double DepthFraction )
{
FBox Full ( Points ) ;
const double Floor = Full . Max . Z - DepthFraction * Full . GetSize ( ) . Y ;
FBox Box ( ForceInit ) ;
for ( const FVector & Point : Points )
{
if ( Point . Z > = Floor )
{
Box + = Point ;
}
}
2026-10-02 13:24:54 +00:00
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 ;
2026-10-02 13:35:23 +00:00
const double Margin = P . Contains ( TEXT ( " Margin " ) ) ? FCString : : Atod ( * P [ TEXT ( " Margin " ) ] ) : 1.05 ;
const double CraniumDepth = P . Contains ( TEXT ( " Cranium " ) ) ? FCString : : Atod ( * P [ TEXT ( " Cranium " ) ] ) : 0.3 ;
2026-10-02 13:24:54 +00:00
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 ;
}
2026-10-02 13:35:23 +00:00
const TArray < FVector > BodyPointsList = HeadPoints ( Body , BodyHead ) ;
const TArray < FVector > FitPointsList = StaticPoints ( FitHead ) ;
const int32 HeadVerts = BodyPointsList . Num ( ) ;
if ( HeadVerts = = 0 | | FitPointsList . Num ( ) = = 0 )
2026-10-02 13:24:54 +00:00
{
2026-10-02 13:35:23 +00:00
UE_LOG ( LogLPHairFromStatic , Error , TEXT ( " Mesure impossible : %d sommets de tete sur le corps, %d sur la tete de reference " ) , HeadVerts , FitPointsList . Num ( ) ) ;
2026-10-02 13:24:54 +00:00
return 1 ;
}
2026-10-02 13:35:23 +00:00
// Ajustement sur le haut du crane (largeur, profondeur, centre) et le sommet : la boite complete inclut oreilles,
// nez et cou, et donnait une coiffure trop etroite enfoncee dans le crane (premier essai O7e).
const FBox BodyFull ( BodyPointsList ) ;
const FBox FitFull ( FitPointsList ) ;
const FBox BodyBox = CraniumBox ( BodyPointsList , CraniumDepth ) ;
const FBox FitBox = CraniumBox ( FitPointsList , CraniumDepth ) ;
// Echelle par axe (largeur X, profondeur Y), hauteur : moyenne des deux ; -Uniform : echelle unique (largeur).
// Marge : la calotte reste au-dessus du cuir chevelu.
2026-10-02 13:24:54 +00:00
const double ScaleX = BodyBox . GetSize ( ) . X / FitBox . GetSize ( ) . X ;
const double ScaleY = Switches . Contains ( TEXT ( " Uniform " ) ) ? ScaleX : BodyBox . GetSize ( ) . Y / FitBox . GetSize ( ) . Y ;
2026-10-02 13:35:23 +00:00
const FVector Scale = FVector ( ScaleX , ScaleY , ( ScaleX + ScaleY ) * 0.5 ) * Margin * ExtraScale ;
2026-10-02 13:24:54 +00:00
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 ;
2026-10-02 13:35:23 +00:00
UE_LOG ( LogLPHairFromStatic , Warning , TEXT ( " Tete du corps (%d sommets) : taille %s ; haut du crane %s, taille %s " ) , HeadVerts , * BodyFull . GetSize ( ) . ToString ( ) , * BodyBox . ToString ( ) , * BodyBox . GetSize ( ) . ToString ( ) ) ;
UE_LOG ( LogLPHairFromStatic , Warning , TEXT ( " Tete de reference : taille %s ; haut du crane %s, taille %s " ) , * FitFull . GetSize ( ) . ToString ( ) , * FitBox . ToString ( ) , * FitBox . GetSize ( ) . ToString ( ) ) ;
2026-10-02 13:24:54 +00:00
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 ;
}