diff --git a/Docs/Migration/13_Protocole_Coiffure.md b/Docs/Migration/13_Protocole_Coiffure.md index 2bda904..04bf236 100644 --- a/Docs/Migration/13_Protocole_Coiffure.md +++ b/Docs/Migration/13_Protocole_Coiffure.md @@ -11,7 +11,8 @@ Durée : environ 30 minutes pour une coiffure, contrôles en jeu compris. | Exigence | Raison | |---|---| | `SkeletalMesh` dont les os portent les noms Genesis 8 (`hip`, `pelvis`… `neckUpper`, `head`) | `Person` fait suivre la coiffure au corps par `SetLeaderPoseComponent`, qui associe les os **par leur nom** | -| Placée autour de la tête de Bridget, dans la pose de référence | les sommets sont liés à l'os `head` ; une coiffure mal placée flotte ou traverse le crâne | +| Placée autour de la tête de Bridget, dans la pose de référence | une coiffure mal placée flotte ou traverse le crâne | +| Poids de peau répartis sur tête, cou, clavicules et poitrine | une coiffure longue liée à la seule tête tourne d'un bloc avec elle et ses mèches traversent épaules et poitrine | | Matériaux enfants de `/Game/Heavy/Daz/Bridget/Hairs/DazHair` | le jeu règle la couleur et l'épaisseur par les paramètres `HairColor` et `HairThickness` | | Déclarée dans un fichier `Modules//**/*.lphair` | la liste des coiffures du jeu est construite depuis ces fichiers (lots H1, H2) ; un maillage non déclaré n'est jamais utilisé | | Nom d'objet unique | les personnages désignent leur coiffure par ce nom (`HAIR Hairstyle: ` dans les `.lpcharacter`) | @@ -87,6 +88,13 @@ Contrôler les lignes `LogLPHairFromStatic` : Puis sans `-DryRun`. La commandlet refuse d'écraser un asset existant : pour refaire, déplacer d'abord `` et `_Skeleton` vers la sauvegarde. +**Liaison au squelette** (`-Skin`, défaut `transfer`) : chaque sommet de la coiffure reçoit les poids des 6 sommets du +corps les plus proches (`-Neighbors`), moyennés selon la distance, 4 influences au plus ; les os des bras et des jambes +sont ramenés à leur ancêtre (une mèche ne suit pas un bras). Tout sommet à moins de 3 mm de la peau, ou dessous, est +repoussé le long de la normale du corps (`-Gap`, en cm). Résultat type (Pocolov 06) : tête 61 %, haut de la poitrine +10 %, clavicules 14 %, cou 4 %, pectoraux 3 % ; 660 sommets sur 5 618 sortis de la peau. `-Skin=rigid` : tout sur l'os +`head` (première version, à réserver aux coiffures courtes). + Paramètres de retouche (après un premier contrôle en jeu) : `-OffsetZ=` (cm, monter/descendre), `-OffsetY=` (cm, avancer/reculer), `-Scale=` (facteur global), `-Margin=` (défaut 1,05 : écart calotte/cuir chevelu), `-Cranium=` (défaut 0,3 : hauteur mesurée sous le sommet, en fraction de la profondeur de la tête), `-Uniform` (même échelle sur X et Y). @@ -162,6 +170,7 @@ Puis : entrée CHG dans `Journal_Modifications.md`, commit, tags `-apres` / | Affectation des matériaux en Python | `save_loaded_asset` ne sauve que les assets « modifiés » ; une affectation par propriété ne les marque pas | sauvegarder avec `only_if_is_dirty=False` (fait par `HairMaterials.py`) | | Matériaux importés du FBX | souvent sans texture reliée | ne pas importer les matériaux (`HairImportFbx.py`) ; recréer avec `HairMaterials.py` | | Comparer des noms d'assets | `soldier` et `Soldier` désignent le même asset | comparaisons insensibles à la casse | +| Liaison rigide à la tête (première version) | cheveux longs : les mèches tournent avec la tête et traversent épaules et poitrine (vidéo utilisateur) | transfert de poids depuis le corps et sortie de la peau (`-Skin=transfer`, défaut) | ## 5. Cas particuliers @@ -170,7 +179,9 @@ Puis : entrée CHG dans `Journal_Modifications.md`, commit, tags `-apres` / - **Coiffure masculine** : `SEX: M` dans le `.lphair` (le menu du personnage masculin la propose). - **Morphs de forme** : non générés (la coiffure ne suit pas les morphs du visage). Un morph existant de la coiffure peut être déclaré avec `FIT_MORPH: `. -- **Mèches animées** : non gérées (coiffure rigide sur la tête). +- **Mèches animées** : pas de simulation physique ; les mèches suivent tête, cou et poitrine par leurs poids. +- **Morphs du corps** : la sortie de la peau est calculée sur le corps de base ; une poitrine ou des épaules agrandies + par les morphs peuvent encore traverser les mèches les plus proches. - **Coiffure apportée par un mod sans recompiler le jeu** : impossible aujourd'hui (le maillage doit être cuit avec le jeu) ; étape H3 prévue. @@ -186,7 +197,8 @@ Les étapes 0 à 6 sont enchaînées pour un **lot** de coiffures décrit par un "cap_opacity": "white", "thicken": 1 } ] } ``` -Options par coiffure : `margin`, `scale`, `offset_z`, `offset_y` (transmises à `LPHairFromStaticMesh`). +Options par coiffure : `margin`, `scale`, `offset_z`, `offset_y`, `skin`, `gap` (transmises à `LPHairFromStaticMesh`). +`-Rebuild` refait une coiffure déjà convertie (la version précédente est déplacée dans `\\precedente_`). ```powershell .\Tools\HairPipeline.ps1 -Config Tools\hairs\.json -Step Inspect # puis relire \rapport_Inspect.txt diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index bfe3e75..adff459 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -937,3 +937,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol | CHG-090 | 2026-10-02 | H2b | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (clés `FACIAL_HAIR`, `BODY_HAIR_M`, `BODY_HAIR_F`) ; `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (+ 44 noms) ; `11_Audit_Optimisation.md` §16.10 | pilosité sortie du Blueprint (demande utilisateur) : listes `GI.FHairstyles`, `BHairstyles_M`, `BHairstyles` ; `GI.OtherTable` laissée (maillages absents depuis 4.27) ; vérifié : 3 listes identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (28 barbes + sans barbe dans le menu de création) | retirer les lignes `*_HAIR` de `vin_Base.lphair` ; `git checkout H2b-avant -- Source/LifePlay` | | CHG-091 | 2026-10-02 | Doc | nouveaux `Docs/Migration/13_Protocole_Coiffure.md`, `Tools/HairImportFbx.py`, `Tools/HairMaterials.py` ; `README.md` (migration), `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md` | protocole de conversion d'une coiffure (demande utilisateur) ; scripts ponctuels d'O7e rendus paramétrables ; protocole rejoué de bout en bout sur Pocolov 17 sous le nom `Pocolov17P` : bornes identiques au millimètre, dépendances OK ; essai retiré du projet (`D:\Game\LifePlay_Backup_5.5\O7e\proto_essai`) | `git revert` du commit | | CHG-092 | 2026-10-02 | HAIRS1 | nouveaux `Tools/HairPipeline.ps1`, `Tools/FbxMaterialMap.py`, `Tools/hairs/HAIRS1.json` ; `Tools/HairImportFbx.py` (rapport JSON), `Tools/HairMaterials.py` (calotte seulement si utilisée, contrôle des dépendances) ; assets `Content/Heavy/New5/Hairs/Pocolov03/06/11/15/16*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov03/06/11/15/16.lphair` ; `Licenses/Free_3D_Models_Credits.txt` ; `13_Protocole_Coiffure.md` §7 | industrialisation de la conversion (demande utilisateur) et test sur 5 coiffures Pocolov (CC BY 4.0, téléchargées avec l'accord de l'utilisateur) ; 5 cas d'archives différents intégrés aux règles ; build `LifePlay_Build_5.5_HAIRS1` : 6 coiffures ajoutées (index 126 à 131) ; contrôle visuel en jeu en attente | retirer les `.lphair` ; assets et sources dans `D:\Game\LifePlay_Backup_5.5\HAIRS1` ; `git checkout HAIRS1-avant -- Tools Licenses Content/Modules` | +| CHG-093 | 2026-10-02 | SKIN | `LPHairFromStaticMeshCommandlet` : liaison par transfert de poids depuis le corps (`-Skin=transfer` par défaut, 6 voisins, bras et jambes exclus) et sortie de la peau (`-Gap` 0,3 cm) ; `LifePlayEditorTools.Build.cs` (`SkeletalMeshDescription`, `AnimationCore`) ; `HairPipeline.ps1` (`-Rebuild`, options `skin`, `gap`) ; 6 coiffures Pocolov reconstruites (versions précédentes dans `HAIRS1\\precedente_*`), Pocolov 17 ajoutée au lot ; `.lphair` et crédits : description des modifications corrigée ; `13_Protocole_Coiffure.md` | vidéo utilisateur : mèches longues liées à la seule tête, qui traversent épaules et poitrine quand la tête tourne ; poids obtenus : tête 47 à 62 %, cou, clavicules, poitrine ; 254 à 950 sommets sortis de la peau par coiffure ; build `LifePlay_Build_5.5_SKIN` ; contrôle en jeu en attente | `-Skin=rigid` ; `git checkout SKIN-avant -- Source Tools` et remettre les versions précédentes | diff --git a/Licenses/Free_3D_Models_Credits.txt b/Licenses/Free_3D_Models_Credits.txt index 16e5cb2..a5665c4 100644 --- a/Licenses/Free_3D_Models_Credits.txt +++ b/Licenses/Free_3D_Models_Credits.txt @@ -26,9 +26,9 @@ In no particular order, these are the authors of the original (most have been he - Joel Palmius LifePlay Revival (UE 5.5) additions: -- Pocolov Hair 17 by Pocolov (Pocolov Studio 2019, artist Thang), Creative Commons Attribution 4.0 (CC BY 4.0), https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (Content/Heavy/New5/Hairs/Pocolov17*, declared by Content/Modules/lp_Revival/Hairstyles/pocolov17.lphair). -- Pocolov Hair 03 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3 . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov03*). -- Pocolov Hair 06 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428 . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov06*). -- Pocolov Hair 11 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79 . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov11*). -- Pocolov Hair 15 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov15*). -- Pocolov Hair 16 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov16*). +- Pocolov Hair 17 by Pocolov (Pocolov Studio 2019, artist Thang), Creative Commons Attribution 4.0 (CC BY 4.0), https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (Content/Heavy/New5/Hairs/Pocolov17*, declared by Content/Modules/lp_Revival/Hairstyles/pocolov17.lphair). +- Pocolov Hair 03 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov03*). +- Pocolov Hair 06 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov06*). +- Pocolov Hair 11 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov11*). +- Pocolov Hair 15 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov15*). +- Pocolov Hair 16 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/Game/Heavy/New5/Hairs/Pocolov16*). diff --git a/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.cpp b/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.cpp index b608f91..e6539d4 100644 --- a/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.cpp +++ b/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.cpp @@ -11,6 +11,7 @@ #include "Rendering/SkeletalMeshLODModel.h" #include "Rendering/SkeletalMeshModel.h" #include "StaticMeshAttributes.h" +#include "SkeletalMeshAttributes.h" #include "StaticToSkeletalMeshConverter.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" @@ -101,6 +102,75 @@ namespace return Box; } + /** Sommet du corps pour le transfert de poids : position, normale, poids exprimes en os du squelette de la coiffure. */ + struct FBodySample + { + FVector Position; + FVector Normal; + TArray, TInlineAllocator<8>> Weights; + }; + + /** Os des membres : une meche ne doit pas suivre un bras ni une jambe ; ces os sont ramenes a leur ancetre. */ + bool IsLimbBone(const FString& Name) + { + static const TCHAR* Parts[] = { TEXT("Shldr"), TEXT("Forearm"), TEXT("Hand"), TEXT("Carpal"), TEXT("Thumb"), TEXT("Index"), + TEXT("Mid"), TEXT("Ring"), TEXT("Pinky"), TEXT("Thigh"), TEXT("Shin"), TEXT("Foot"), TEXT("Toe"), TEXT("Metatarsal") }; + for (const TCHAR* Part : Parts) + { + if (Name.Contains(Part)) + { + return true; + } + } + return false; + } + + /** Sommets du corps au-dessus de MinZ, poids convertis vers les os du squelette cible (ancetre le plus proche present). */ + TArray BodySamples(const USkeletalMesh* Body, const FReferenceSkeleton& Target, double MinZ) + { + const FReferenceSkeleton& Ref = Body->GetRefSkeleton(); + TArray BoneMap; + BoneMap.SetNum(Ref.GetRawBoneNum()); + for (int32 Bone = 0; Bone < Ref.GetRawBoneNum(); ++Bone) + { + int32 Mapped = 0; + for (int32 B = Bone; B != INDEX_NONE; B = Ref.GetParentIndex(B)) + { + const FString Name = Ref.GetBoneName(B).ToString(); + const int32 T = IsLimbBone(Name) ? INDEX_NONE : Target.FindBoneIndex(*Name); + if (T != INDEX_NONE) + { + Mapped = T; + break; + } + } + BoneMap[Bone] = Mapped; + } + TArray Samples; + const FSkeletalMeshLODModel& LOD = Body->GetImportedModel()->LODModels[0]; + for (const FSkelMeshSection& Section : LOD.Sections) + { + for (const FSoftSkinVertex& V : Section.SoftVertices) + { + if (V.Position.Z < MinZ) + { + continue; + } + FBodySample& Sample = Samples.AddDefaulted_GetRef(); + Sample.Position = FVector(V.Position); + Sample.Normal = FVector(FVector3f(V.TangentZ)).GetSafeNormal(); + for (int32 i = 0; i < MAX_TOTAL_INFLUENCES; ++i) + { + if (V.InfluenceWeights[i] > 0 && Section.BoneMap.IsValidIndex(V.InfluenceBones[i])) + { + Sample.Weights.Add({ BoneMap[Section.BoneMap[V.InfluenceBones[i]]], V.InfluenceWeights[i] / 65535.f }); + } + } + } + } + return Samples; + } + /** Point de reference d'une tete : centre en X et Y, sommet du crane en Z. */ FVector TopCenter(const FBox& Box) { @@ -146,6 +216,12 @@ int32 ULPHairFromStaticMeshCommandlet::Main(const FString& Params) 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; const bool bDryRun = Switches.Contains(TEXT("DryRun")); + // Liaison : "transfer" (defaut) = poids copies des sommets du corps les plus proches, sommets sous la peau repousses ; + // "rigid" = tout sur l'os -Bone (premiere version ; les meches longues traversent epaules et poitrine quand la tete + // tourne). + const bool bRigid = P.FindRef(TEXT("Skin")).Equals(TEXT("rigid"), ESearchCase::IgnoreCase); + const double Gap = P.Contains(TEXT("Gap")) ? FCString::Atod(*P[TEXT("Gap")]) : 0.3; + const int32 Neighbors = P.Contains(TEXT("Neighbors")) ? FCString::Atoi(*P[TEXT("Neighbors")]) : 6; if (StaticPath.IsEmpty() || FitHeadPath.IsEmpty() || OutPath.IsEmpty()) { UE_LOG(LogLPHairFromStatic, Error, TEXT("Usage : -Static= -FitHead= -Out= [-Body=] [-RefHair=] [-Bone=head] [-Scale=] [-OffsetZ=] [-OffsetY=] [-DryRun]")); @@ -225,13 +301,90 @@ int32 ULPHairFromStaticMeshCommandlet::Main(const FString& Params) Placed += Final; } UE_LOG(LogLPHairFromStatic, Warning, TEXT("Coiffure placee : %s (avant : %s)"), *Placed.ToString(), *Static->GetBoundingBox().ToString()); + + // 2b. Transfert de poids depuis le corps et sortie de la peau. + TMap> VertexWeights; + if (!bRigid) + { + const TArray Samples = BodySamples(Body, HairRef, Placed.Min.Z - 15.0); + int32 Pushed = 0; + TMap BoneShare; + for (const FVertexID Vertex : Desc->Vertices().GetElementIDs()) + { + FVector Position(Positions[Vertex]); + // k plus proches sommets du corps (recherche exhaustive : quelques milliers x quelques dizaines de milliers). + TArray, TInlineAllocator<16>> Nearest; + for (int32 i = 0; i < Samples.Num(); ++i) + { + const double D2 = FVector::DistSquared(Position, Samples[i].Position); + if (Nearest.Num() < Neighbors || D2 < Nearest.Last().Key) + { + int32 At = Nearest.Num(); + while (At > 0 && Nearest[At - 1].Key > D2) { --At; } + Nearest.Insert(TPair(D2, i), At); + if (Nearest.Num() > Neighbors) { Nearest.Pop(EAllowShrinking::No); } + } + } + if (Nearest.Num() == 0) + { + continue; + } + // Sortie de la peau : un sommet a moins de Gap devant la surface (ou derriere) est repousse le long de la normale. + const FBodySample& Closest = Samples[Nearest[0].Value]; + const double Depth = FVector::DotProduct(Position - Closest.Position, Closest.Normal); + if (Depth < Gap && FMath::Sqrt(Nearest[0].Key) < 4.0) + { + Position += Closest.Normal * (Gap - Depth); + Positions[Vertex] = FVector3f(Position); + ++Pushed; + } + // Poids : moyenne des voisins ponderee par l'inverse de la distance, 4 influences au plus. + TMap Accum; + for (const TPair& N : Nearest) + { + const float W = 1.f / (float(FMath::Sqrt(N.Key)) + 0.5f); + for (const TPair& BW : Samples[N.Value].Weights) + { + Accum.FindOrAdd(BW.Key) += BW.Value * W; + } + } + Accum.ValueSort([](float A, float B) { return A > B; }); + // Normalisation avant creation des poids : FBoneWeight borne sa valeur a 1. + TArray, TInlineAllocator<4>> Top; + float Total = 0.f; + for (const TPair& A : Accum) + { + if (Top.Num() == 4) { break; } + Top.Add(A); + Total += A.Value; + } + TArray Weights; + for (const TPair& T : Top) + { + Weights.Add(UE::AnimationCore::FBoneWeight(FBoneIndexType(T.Key), T.Value / Total)); + BoneShare.FindOrAdd(T.Key) += T.Value / Total; + } + VertexWeights.Add(Vertex, MoveTemp(Weights)); + } + BoneShare.ValueSort([](double A, double B) { return A > B; }); + FString Shares; + for (const TPair& S : BoneShare) + { + Shares += FString::Printf(TEXT("%s %.0f%%, "), *HairRef.GetBoneName(S.Key).ToString(), 100.0 * S.Value / FMath::Max(1, VertexWeights.Num())); + } + UE_LOG(LogLPHairFromStatic, Warning, TEXT("Transfert de poids : %d sommets du corps, %d sommets de coiffure, %d repousse(s) hors de la peau (%.1f mm) ; os : %s"), + Samples.Num(), VertexWeights.Num(), Pushed, Gap * 10.0, *Shares.LeftChop(2)); + } if (bDryRun) { UE_LOG(LogLPHairFromStatic, Warning, TEXT("-DryRun : rien n'est cree")); return 0; } - Work->CommitMeshDescription(0); - Work->Build(true); + if (bRigid) + { + Work->CommitMeshDescription(0); + Work->Build(true); + } // 3. Squelette (copie de celui de la coiffure de reference) et SkeletalMesh. const FString OutName = FPackageName::GetShortName(OutPath); @@ -242,7 +395,41 @@ int32 ULPHairFromStaticMeshCommandlet::Main(const FString& Params) UPackage* MeshPackage = CreatePackage(*OutPath); USkeletalMesh* Hair = NewObject(MeshPackage, *OutName, RF_Public | RF_Standalone); - if (!FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromStaticMesh(Hair, Work, Skeleton->GetReferenceSkeleton(), BoneName)) + bool bConverted = false; + if (bRigid) + { + bConverted = FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromStaticMesh(Hair, Work, Skeleton->GetReferenceSkeleton(), BoneName); + } + else + { + // Description du maillage (positions placees et repoussees) + poids de peau ; materiaux de la coiffure statique. + FMeshDescription SkinnedDesc(*Desc); + FSkeletalMeshAttributes Attributes(SkinnedDesc); + Attributes.Register(true); + FSkinWeightsVertexAttributesRef SkinWeights = Attributes.GetVertexSkinWeights(); + const int32 HeadIndex = HairRef.FindBoneIndex(BoneName); + for (const FVertexID Vertex : SkinnedDesc.Vertices().GetElementIDs()) + { + const TArray* Weights = VertexWeights.Find(Vertex); + if (Weights && Weights->Num() > 0) + { + SkinWeights.Set(Vertex, *Weights); + } + else + { + const UE::AnimationCore::FBoneWeight HeadOnly(FBoneIndexType(HeadIndex), 1.0f); + SkinWeights.Set(Vertex, MakeArrayView(&HeadOnly, 1)); + } + } + TArray Materials; + for (const FStaticMaterial& M : Static->GetStaticMaterials()) + { + Materials.Add(FSkeletalMaterial(M.MaterialInterface, M.MaterialSlotName, M.ImportedMaterialSlotName)); + } + const FMeshDescription* Descs[] = { &SkinnedDesc }; + bConverted = FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromMeshDescriptions(Hair, Descs, Materials, Skeleton->GetReferenceSkeleton(), false, true); + } + if (!bConverted) { UE_LOG(LogLPHairFromStatic, Error, TEXT("Conversion en SkeletalMesh refusee par le moteur")); return 1; diff --git a/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h b/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h index 55ee1fb..aad3cbe 100644 --- a/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h +++ b/Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h @@ -22,6 +22,9 @@ // -Scale, -OffsetZ, -OffsetY : retouches manuelles appliquees apres l'ajustement automatique (cm). // -Cranium : epaisseur mesuree sous le sommet du crane, en fraction de la profondeur de la tete (defaut 0,3) ; // -Margin : agrandissement de securite de la calotte (defaut 1,05). +// -Skin=transfer (defaut) : chaque sommet recoit les poids des -Neighbors (6) sommets du corps les plus proches +// (bras et jambes exclus) et tout sommet a moins de -Gap (0,3 cm) de la peau, ou dessous, est repousse le long de la +// normale du corps : les meches longues suivent cou, epaules et poitrine. -Skin=rigid : tout sur l'os -Bone. #pragma once diff --git a/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs b/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs index 09c3bb7..faa863b 100644 --- a/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs +++ b/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs @@ -42,7 +42,10 @@ public class LifePlayEditorTools : ModuleRules "MeshDescription", // O7e : LPHairFromStaticMeshCommandlet (conversion maillage statique -> SkeletalMesh) "StaticMeshDescription", - "SkeletalMeshUtilitiesCommon" + "SkeletalMeshUtilitiesCommon", + // Transfert de poids (FSkeletalMeshAttributes, FBoneWeight) + "SkeletalMeshDescription", + "AnimationCore" }); } } diff --git a/Tools/HairPipeline.ps1 b/Tools/HairPipeline.ps1 index 94f68e7..6920578 100644 --- a/Tools/HairPipeline.ps1 +++ b/Tools/HairPipeline.ps1 @@ -23,7 +23,10 @@ Licenses/Free_3D_Models_Credits.txt (une ligne par coiffure, sans doublon) ; puis Tools/BuildCollections.py et un bilan. Le build et le controle en jeu restent manuels (protocole, etapes 6 et 7). - Une coiffure dont le SkeletalMesh existe deja est sautee (rien n'est ecrase). + Une coiffure dont le SkeletalMesh existe deja est sautee (rien n'est ecrase), sauf avec -Rebuild : la coiffure et son + squelette precedents sont alors deplaces dans \\precedente_ avant reconstruction. + Options de LPHairFromStaticMesh par coiffure dans le JSON : margin, scale, offset_z, offset_y, skin (transfer|rigid), + gap (cm). .EXAMPLE .\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Inspect @@ -32,7 +35,8 @@ param( [Parameter(Mandatory)] [string] $Config, [Parameter(Mandatory)] [ValidateSet('Inspect', 'Apply')] [string] $Step, - [string] $Only = '' + [string] $Only = '', + [switch] $Rebuild ) $ErrorActionPreference = 'Stop' @@ -186,10 +190,18 @@ foreach ($h in $cfg.hairs) { else { Write-Host "== $name : application" if (-not $h.inspected) { $report += "$name : non inspectee, sautee"; continue } - if (Test-Path (Join-Path $Root ("Content" + $meshPath.Substring(5) + '.uasset'))) { $report += "$name : $meshPath existe deja, sautee"; continue } + $existing = Join-Path $Root ("Content" + $meshPath.Substring(5) + '.uasset') + if (Test-Path $existing) { + if (-not $Rebuild) { $report += "$name : $meshPath existe deja, sautee (-Rebuild pour la refaire)"; continue } + # -Rebuild : coiffure et squelette precedents deplaces vers la sauvegarde (materiaux et textures mis a jour sur place). + $previous = Join-Path $dir ('precedente_' + (Get-Date -Format 'yyyyMMdd_HHmmss')) + New-Item -ItemType Directory -Force $previous | Out-Null + Move-Item $existing $previous + Move-Item ($existing -replace '\.uasset$', '_Skeleton.uasset') $previous -ErrorAction SilentlyContinue + } $common = @("-Static=$($h.mesh)", "-FitHead=$($h.head)", "-Out=$meshPath") if ($h.margin) { $common += "-Margin=$($h.margin)" } - foreach ($k in 'scale', 'offset_z', 'offset_y') { if ($h.$k) { $common += ('-' + @{ scale = 'Scale'; offset_z = 'OffsetZ'; offset_y = 'OffsetY' }[$k] + '=' + $h.$k) } } + foreach ($k in 'scale', 'offset_z', 'offset_y', 'skin', 'gap') { if ($h.$k) { $common += ('-' + @{ scale = 'Scale'; offset_z = 'OffsetZ'; offset_y = 'OffsetY'; skin = 'Skin'; gap = 'Gap' }[$k] + '=' + $h.$k) } } $dry = Invoke-UE (@('-run=LPHairFromStaticMesh') + $common + '-DryRun') 'LogLPHairFromStatic: (Warning|Error)' $dry | Write-Host $scaleLine = $dry | Where-Object { $_ -match 'echelle X=([\d.]+) Y=([\d.]+)' } | Select-Object -First 1 @@ -212,12 +224,12 @@ foreach ($h in $cfg.hairs) { $lines = @( "// $($h.title) - $($h.author) - licence $($h.license)", "// Source : $($h.url)", - '// Modifications (Tools/HairPipeline.ps1) : rattachee au squelette Genesis 8 (os head), ajustee a la tete de Bridget, materiaux DazHair.', + '// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.', "HAIRSTYLE: $meshPath.$name", "SEX: $($h.sex)", 'DAZ: true') [IO.File]::WriteAllText((Join-Path $lphairDir "$($name.ToLower()).lphair"), ($lines -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false)) $credits = Join-Path $Root 'Licenses\Free_3D_Models_Credits.txt' if (-not (Select-String -LiteralPath $credits -SimpleMatch $h.url -Quiet)) { - Add-Content -LiteralPath $credits -Value ("- $credit . Changes: bound to the Genesis 8 skeleton (head bone), fitted to the Bridget head, DazHair materials ($meshPath*).") -Encoding utf8 + Add-Content -LiteralPath $credits -Value ("- $credit . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials ($meshPath*).") -Encoding utf8 } $report += "$name : OK, echelle X=$sx Y=$sy, bornes $placed, dependances $(if ($depOk) { 'OK' } else { 'A VERIFIER' })" } diff --git a/Tools/hairs/HAIRS1.json b/Tools/hairs/HAIRS1.json index 62dabe4..5521f40 100644 --- a/Tools/hairs/HAIRS1.json +++ b/Tools/hairs/HAIRS1.json @@ -122,6 +122,30 @@ "strands_opacity" ], "inspected": true + }, + { + "name": "Pocolov17", + "archive": "D:/Game/pocolov-hair-17.zip", + "title": "Pocolov Hair 17", + "url": "https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d", + "mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov17/Hair_17", + "head": "/Game/Heavy/New5/Hairs/_Work_Pocolov17/Head", + "cap_slots": "Hair_Base", + "diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov17/norm/tex01_Hair_01_Gray.png", + "strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov17/norm/tex02_Hair_01A1.jpg", + "cap_opacity": "white", + "thicken": 1, + "sex": "F", + "license": "CC BY 4.0", + "author": "Pocolov (Pocolov Studio 2019, artist Thang)", + "auto_fields": [ + "mesh", + "head", + "cap_slots", + "diffuse", + "strands_opacity" + ], + "inspected": true } ] }