SKIN : coiffures liees au corps par transfert de poids et sorties de la peau

Les meches longues suivent cou, clavicules et poitrine au lieu de tourner avec la tete ;
6 coiffures Pocolov reconstruites (-Rebuild).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-02 19:36:28 +02:00
parent 27734009a2
commit ac7de7d2c2
8 changed files with 261 additions and 19 deletions

View file

@ -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/<module>/**/*.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: <nom>` 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 `<Nom>` et
`<Nom>_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 `<lot>-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 `<lot>-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: <morph> <valeur>`.
- **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 `<backup>\<nom>\precedente_<date>`).
```powershell
.\Tools\HairPipeline.ps1 -Config Tools\hairs\<lot>.json -Step Inspect # puis relire <backup>\rapport_Inspect.txt

View file

@ -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\<nom>\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 |

View file

@ -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*).

View file

@ -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<TPair<int32, float>, 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<FBodySample> BodySamples(const USkeletalMesh* Body, const FReferenceSkeleton& Target, double MinZ)
{
const FReferenceSkeleton& Ref = Body->GetRefSkeleton();
TArray<int32> 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<FBodySample> 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=<StaticMesh> -FitHead=<StaticMesh> -Out=</Game/...> [-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<FVertexID, TArray<UE::AnimationCore::FBoneWeight>> VertexWeights;
if (!bRigid)
{
const TArray<FBodySample> Samples = BodySamples(Body, HairRef, Placed.Min.Z - 15.0);
int32 Pushed = 0;
TMap<int32, double> 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<TPair<double, int32>, 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<double, int32>(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<int32, float> Accum;
for (const TPair<double, int32>& N : Nearest)
{
const float W = 1.f / (float(FMath::Sqrt(N.Key)) + 0.5f);
for (const TPair<int32, float>& 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<TPair<int32, float>, TInlineAllocator<4>> Top;
float Total = 0.f;
for (const TPair<int32, float>& A : Accum)
{
if (Top.Num() == 4) { break; }
Top.Add(A);
Total += A.Value;
}
TArray<UE::AnimationCore::FBoneWeight> Weights;
for (const TPair<int32, float>& 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<int32, double>& 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<USkeletalMesh>(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<UE::AnimationCore::FBoneWeight>* 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<FSkeletalMaterial> 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;

View file

@ -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

View file

@ -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"
});
}
}

View file

@ -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 <backup>\<nom>\precedente_<date> 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' })"
}

View file

@ -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
}
]
}