HAIRS1 : chaine industrialisee de conversion des coiffures (HairPipeline.ps1, FbxMaterialMap.py) testee sur 5 coiffures Pocolov
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2744242609
commit
2479a0af9e
8 changed files with 531 additions and 4 deletions
|
|
@ -27,6 +27,7 @@ depuis O1). Une coiffure déjà riggée Genesis 8 (export Daz) n'a pas besoin de
|
|||
| `Tools/HairImportFbx.py` | import du FBX en maillages statiques séparés, dans un dossier de travail ; affiche tailles et emplacements de matériaux |
|
||||
| commandlet `LPHairFromStaticMesh` (`Source/LifePlayEditorTools`) | ajuste la coiffure sur la tête de Bridget et la convertit en `SkeletalMesh` Genesis 8 |
|
||||
| `Tools/HairMaterials.py` | importe les textures, crée les matériaux `<Nom>_Cap` et `<Nom>_Strands`, les affecte, contrôle les dépendances |
|
||||
| `Tools/HairPipeline.ps1`, `Tools/FbxMaterialMap.py` | chaîne industrialisée d'un lot de coiffures (§7) |
|
||||
| `Tools/BuildCollections.py` | range la coiffure dans la collection `LifePlay_Cheveux` de l'éditeur |
|
||||
| option de jeu `-LPVerifyHairstyles` | journalise la construction de la liste des coiffures |
|
||||
|
||||
|
|
@ -173,6 +174,54 @@ Puis : entrée CHG dans `Journal_Modifications.md`, commit, tags `<lot>-apres` /
|
|||
- **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.
|
||||
|
||||
## 7. Chaîne industrialisée (`Tools/HairPipeline.ps1`)
|
||||
|
||||
Les étapes 0 à 6 sont enchaînées pour un **lot** de coiffures décrit par un fichier JSON (`Tools/hairs/<lot>.json`) :
|
||||
|
||||
```json
|
||||
{ "module": "lp_Revival", "folder": "/Game/Heavy/New5/Hairs", "backup": "D:/Game/LifePlay_Backup_5.5/<lot>",
|
||||
"hairs": [ { "name": "Pocolov15", "archive": "D:/Game/pocolov-hair-15.zip", "title": "Pocolov Hair 15",
|
||||
"author": "...", "license": "CC BY 4.0", "url": "https://www.fab.com/listings/...", "sex": "F",
|
||||
"mesh": "auto", "head": "auto", "cap_slots": "auto", "diffuse": "auto", "strands_opacity": "auto",
|
||||
"cap_opacity": "white", "thicken": 1 } ] }
|
||||
```
|
||||
|
||||
Options par coiffure : `margin`, `scale`, `offset_z`, `offset_y` (transmises à `LPHairFromStaticMesh`).
|
||||
|
||||
```powershell
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\<lot>.json -Step Inspect # puis relire <backup>\rapport_Inspect.txt
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\<lot>.json -Step Apply # [-Only <nom>] pour une seule coiffure
|
||||
```
|
||||
|
||||
**Inspect** : extraction (zip, rar, 7z, archives imbriquées, avec 7-Zip), copie du FBX et des textures sous des noms
|
||||
sans espaces, import (`HairImportFbx.py`), lecture des associations matériau → textures du FBX
|
||||
(`Tools/FbxMaterialMap.py`, lecteur de FBX binaire sans SDK), puis détection des champs `auto` :
|
||||
|
||||
| Champ | Règle |
|
||||
|---|---|
|
||||
| `mesh` | plus gros maillage qui n'est pas la tête (nom quelconque : `Box001` pour Pocolov 03) |
|
||||
| `head` | maillage dont le nom contient `head` ; sinon la coiffure elle-même (et `margin` 1,0) |
|
||||
| `cap_slots` | emplacements nommés `base`, `cap` ou `scalp`, **seulement s'il reste un autre emplacement** pour les mèches |
|
||||
| `diffuse` | parmi les textures nommées `hair` (si l'archive contient une tête complète : peau, yeux…) : non masque, grise de préférence, la plus grande |
|
||||
| `strands_opacity` | masque (gris, noir/blanc) **vertical** (un masque carré est la lisière de la calotte) ; préférence au nom d'un emplacement des mèches, puis aux bords latéraux sombres (carte à bords transparents) ; emplacement unique : masque déclaré par le FBX |
|
||||
|
||||
Les champs détectés sont mémorisés dans `auto_fields` et recalculés à chaque inspection ; une valeur saisie à la main
|
||||
n'est jamais écrasée. Le rapport liste chaque texture (taille, saturation, part noir/blanc, bas et bords sombres) et
|
||||
les associations du FBX.
|
||||
|
||||
**Apply** : essai à blanc et contrôle des échelles (entre 0,6 et 1,8, sinon la coiffure est sautée), conversion,
|
||||
matériaux (`HairMaterials.py` ; matériau de calotte créé seulement s'il sert), dossier de travail déplacé vers la
|
||||
sauvegarde, `.lphair` avec attribution, ligne de crédits (sans doublon), collections, bilan. Une coiffure dont le
|
||||
`SkeletalMesh` existe déjà est sautée : la chaîne peut être relancée après une interruption.
|
||||
|
||||
**Premier lot (HAIRS1, 2026-10-02)** : Pocolov Hair 03, 06, 11, 15, 16 (CC BY 4.0). Cas rencontrés et intégrés aux
|
||||
règles : modèle dans un RAR (03) ; archive avec tête complète, textures de peau et d'yeux (03) ; maillage nommé
|
||||
`Box001` (03) ; masque carré pris pour celui des mèches (16) ; emplacement unique (11) ; masques `A`/`A1` voisins (03,
|
||||
15). Résultat : 5 coiffures conformes (dépendances limitées au squelette et aux matériaux), toutes ajoutées par le jeu.
|
||||
|
||||
**Licences** : seules les licences permettant la redistribution (CC BY, CC0) conviennent, l'archive des sources
|
||||
publiée contenant `Content/Heavy`. La licence standard Fab (« personal/professional ») ne le permet pas.
|
||||
|
||||
## 6. Retour arrière
|
||||
|
||||
Retirer le fichier `.lphair` suffit à retirer la coiffure du jeu. Pour retirer aussi les assets : déplacer
|
||||
|
|
|
|||
|
|
@ -936,3 +936,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
|
|||
| CHG-089 | 2026-10-02 | H2 | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (mode base, plusieurs coiffures par fichier, `NONE`, `MORPHABLE`, vérification `-LPVerifyHairstyles`) ; nouveau `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (126 coiffures, 89 morphs) ; `11_Audit_Optimisation.md` §16.9 | liste de base des coiffures sortie du Blueprint (demande utilisateur) ; vérifié : table et morphs reconstruits identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (nouvelle partie, liste du menu inchangée avec Pocolov17 en dernier, personnages générés, Continue) | supprimer `vin_Base.lphair` (retour à la table du Blueprint) ; `git checkout H2-avant -- Source/LifePlay` |
|
||||
| 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` |
|
||||
|
|
|
|||
|
|
@ -27,3 +27,8 @@ In no particular order, these are the authors of the original (most have been he
|
|||
|
||||
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*).
|
||||
|
|
|
|||
97
Tools/FbxMaterialMap.py
Normal file
97
Tools/FbxMaterialMap.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Lecture des associations materiau -> textures d'un FBX binaire, sans SDK (Tools/HairPipeline.ps1).
|
||||
#
|
||||
# Un FBX binaire est un arbre de noeuds ; Objects contient les Material, Texture (RelativeFilename) et Video, et
|
||||
# Connections les liens "OP" (texture -> propriete d'un materiau : DiffuseColor, TransparentColor, OpacityMap...) ou
|
||||
# "OO" (video -> texture). Ce script les relie et ecrit, pour chaque materiau, le fichier de chaque canal.
|
||||
#
|
||||
# Usage : python Tools/FbxMaterialMap.py <fichier.fbx> [<sortie.json>]
|
||||
# Sortie : {"<materiau>": {"DiffuseColor": "Hair_01_Gray.png", "TransparentColor": "Hair_01A1.jpg", ...}, ...}
|
||||
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
|
||||
def read_nodes(data, offset, end, wide):
|
||||
nodes = []
|
||||
while offset < end:
|
||||
if wide:
|
||||
end_offset, num_props, _ = struct.unpack_from("<QQQ", data, offset)
|
||||
offset += 24
|
||||
else:
|
||||
end_offset, num_props, _ = struct.unpack_from("<III", data, offset)
|
||||
offset += 12
|
||||
name_len = data[offset]
|
||||
offset += 1
|
||||
if end_offset == 0:
|
||||
break
|
||||
name = data[offset:offset + name_len].decode("ascii", "replace")
|
||||
offset += name_len
|
||||
props = []
|
||||
for _ in range(num_props):
|
||||
code = chr(data[offset])
|
||||
offset += 1
|
||||
if code in "YCIFDL":
|
||||
fmt = {"Y": "<h", "C": "<?", "I": "<i", "F": "<f", "D": "<d", "L": "<q"}[code]
|
||||
props.append(struct.unpack_from(fmt, data, offset)[0])
|
||||
offset += struct.calcsize(fmt)
|
||||
elif code in "SR":
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
raw = data[offset:offset + length]
|
||||
props.append(raw.decode("utf-8", "replace") if code == "S" else raw)
|
||||
offset += length
|
||||
elif code in "fdlib":
|
||||
count, encoding, length = struct.unpack_from("<III", data, offset)
|
||||
offset += 12 + length
|
||||
props.append(None) # tableaux inutiles ici
|
||||
else:
|
||||
raise ValueError("type de propriete FBX inconnu : " + code)
|
||||
children = read_nodes(data, offset, end_offset, wide) if offset < end_offset else []
|
||||
nodes.append((name, props, children))
|
||||
offset = end_offset
|
||||
return nodes
|
||||
|
||||
|
||||
def object_name(prop):
|
||||
# "Nom\x00\x01Classe" -> "Nom"
|
||||
return prop.split("\x00")[0] if isinstance(prop, str) else str(prop)
|
||||
|
||||
|
||||
def material_map(path):
|
||||
data = open(path, "rb").read()
|
||||
if not data.startswith(b"Kaydara FBX Binary"):
|
||||
raise ValueError("FBX ASCII non gere : " + path)
|
||||
version = struct.unpack_from("<I", data, 23)[0]
|
||||
nodes = read_nodes(data, 27, len(data), version >= 7500)
|
||||
objects = next((c for n, p, c in nodes if n == "Objects"), [])
|
||||
connections = next((c for n, p, c in nodes if n == "Connections"), [])
|
||||
materials, textures, videos = {}, {}, {}
|
||||
for name, props, children in objects:
|
||||
if not props:
|
||||
continue
|
||||
oid = props[0]
|
||||
if name == "Material":
|
||||
materials[oid] = object_name(props[1])
|
||||
elif name in ("Texture", "Video"):
|
||||
filename = next((c[1][0] for c in children if c[0] in ("RelativeFilename", "FileName") and c[1]), "")
|
||||
target = textures if name == "Texture" else videos
|
||||
target[oid] = re.split(r"[\\/]", filename)[-1] if filename else object_name(props[1])
|
||||
result = {}
|
||||
for name, props, _ in connections:
|
||||
if name != "C" or len(props) < 3:
|
||||
continue
|
||||
if props[0] == "OP" and props[1] in textures and props[2] in materials:
|
||||
channel = props[3] if len(props) > 3 else "?"
|
||||
result.setdefault(materials[props[2]], {})[channel] = textures[props[1]]
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mapping = material_map(sys.argv[1])
|
||||
text = json.dumps(mapping, indent=1, ensure_ascii=False)
|
||||
if len(sys.argv) > 2:
|
||||
open(sys.argv[2], "w", encoding="utf-8").write(text)
|
||||
print(text)
|
||||
|
|
@ -34,6 +34,7 @@ else:
|
|||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
count = 0
|
||||
meshes = []
|
||||
for path in task.imported_object_paths:
|
||||
asset = unreal.load_asset(path)
|
||||
if isinstance(asset, unreal.StaticMesh):
|
||||
|
|
@ -42,5 +43,12 @@ else:
|
|||
slots = [str(m.material_slot_name) for m in asset.static_materials]
|
||||
unreal.log_warning("HAIRIMPORT %s : %d triangles, taille X=%.1f Y=%.1f Z=%.1f cm, min Z=%.1f max Z=%.1f, emplacements %s"
|
||||
% (path, asset.get_num_triangles(0), size.x, size.y, size.z, box.min.z, box.max.z, slots))
|
||||
meshes.append({"path": path.split(".")[0], "name": asset.get_name(), "triangles": asset.get_num_triangles(0),
|
||||
"min": [box.min.x, box.min.y, box.min.z], "max": [box.max.x, box.max.y, box.max.z], "slots": slots})
|
||||
count += 1
|
||||
unreal.log_warning("HAIRIMPORT %d maillage(s) statique(s) importe(s) dans %s" % (count, dest))
|
||||
# Rapport lisible par Tools/HairPipeline.ps1 (option report=<fichier.json>).
|
||||
if args.get("report"):
|
||||
import json
|
||||
with open(args["report"], "w", encoding="utf-8") as f:
|
||||
json.dump({"fbx": fbx, "dest": dest, "meshes": meshes}, f, indent=1)
|
||||
|
|
|
|||
|
|
@ -58,19 +58,24 @@ else:
|
|||
parent = unreal.load_asset("/Game/Heavy/Daz/Bridget/Hairs/DazHair")
|
||||
diffuse = import_texture(args["diffuse"], folder, name + "_Diffuse")
|
||||
strands_opacity = import_texture(args["strands_opacity"], folder, name + "_StrandsOpacity")
|
||||
cap_arg = args.get("cap_opacity", "white")
|
||||
cap_opacity = unreal.load_asset("/Engine/EngineResources/WhiteSquareTexture") if cap_arg == "white" else import_texture(cap_arg, folder, name + "_CapOpacity")
|
||||
strands = make_mic(folder, name + "_Strands", parent, diffuse, strands_opacity, thicken)
|
||||
cap = make_mic(folder, name + "_Cap", parent, diffuse, cap_opacity, 1.0)
|
||||
# Calotte : seulement si un emplacement lui est attribue (Pocolov 11 : emplacement unique, pas de calotte).
|
||||
cap = None
|
||||
if cap_slots:
|
||||
cap_arg = args.get("cap_opacity", "white")
|
||||
cap_opacity = unreal.load_asset("/Engine/EngineResources/WhiteSquareTexture") if cap_arg == "white" else import_texture(cap_arg, folder, name + "_CapOpacity")
|
||||
cap = make_mic(folder, name + "_Cap", parent, diffuse, cap_opacity, 1.0)
|
||||
|
||||
# Affectation : modifier les elements puis reaffecter le tableau entier et sauvegarder sans condition
|
||||
# (une affectation Python ne marque pas l'asset modifie).
|
||||
mesh = unreal.load_asset(mesh_path)
|
||||
materials = []
|
||||
used = set()
|
||||
for slot in mesh.get_editor_property("materials"):
|
||||
slot_name = str(slot.get_editor_property("material_slot_name"))
|
||||
slot.set_editor_property("material_interface", cap if slot_name in cap_slots else strands)
|
||||
materials.append(slot)
|
||||
used.add(folder + "/" + (cap if slot_name in cap_slots else strands).get_name())
|
||||
unreal.log_warning("HAIRMAT emplacement %s -> %s" % (slot_name, (cap if slot_name in cap_slots else strands).get_name()))
|
||||
mesh.set_editor_property("materials", materials)
|
||||
eal.save_loaded_asset(mesh, only_if_is_dirty=False)
|
||||
|
|
@ -79,5 +84,5 @@ else:
|
|||
registry.scan_modified_asset_files([unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_content_dir()) + mesh_path[len("/Game/"):] + ".uasset"])
|
||||
options = unreal.AssetRegistryDependencyOptions(include_soft_package_references=True, include_hard_package_references=True)
|
||||
deps = [str(d) for d in registry.get_dependencies(mesh_path, options) if not str(d).startswith("/Script/")]
|
||||
expected = {mesh_path + "_Skeleton", folder + "/" + name + "_Cap", folder + "/" + name + "_Strands"}
|
||||
expected = {mesh_path + "_Skeleton"} | used
|
||||
unreal.log_warning("HAIRMAT dependances de %s : %s -> %s" % (name, deps, "OK" if set(deps) == expected else "A VERIFIER"))
|
||||
|
|
|
|||
235
Tools/HairPipeline.ps1
Normal file
235
Tools/HairPipeline.ps1
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Chaine industrialisee de conversion des coiffures (Docs/Migration/13_Protocole_Coiffure.md, section 7).
|
||||
|
||||
.DESCRIPTION
|
||||
Pilotee par un fichier JSON (un lot de coiffures). Deux etapes :
|
||||
|
||||
-Step Inspect : pour chaque coiffure
|
||||
1. extrait l'archive (zips imbriques compris) dans <backup>\<nom>\src ; copie le FBX et les textures sous des
|
||||
noms sans espaces ;
|
||||
2. importe le FBX en maillages statiques (Tools/HairImportFbx.py) dans <folder>/_Work_<nom> ;
|
||||
3. propose : maillage de la coiffure, tete de reference (nom contenant "head"), emplacements de calotte (nom
|
||||
contenant base, cap ou scalp), texture de couleur et masque des meches (analyse des images : niveaux de gris,
|
||||
proportion de pixels noirs/blancs, orientation) ;
|
||||
4. ecrit la proposition dans le fichier JSON (champs "auto" remplaces) et un rapport.
|
||||
-> relire le rapport, corriger le JSON si besoin.
|
||||
|
||||
-Step Apply : pour chaque coiffure inspectee
|
||||
1. LPHairFromStaticMesh (essai a blanc, controle des echelles, puis conversion) ;
|
||||
2. Tools/HairMaterials.py (textures, materiaux DazHair, controle des dependances) ;
|
||||
3. dossier _Work_<nom> deplace vers <backup>\<nom>\work (jamais supprime) ;
|
||||
4. Modules/<module>/Hairstyles/<nom>.lphair (attribution comprise) ; credits dans
|
||||
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).
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Inspect
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Apply
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Config,
|
||||
[Parameter(Mandatory)] [ValidateSet('Inspect', 'Apply')] [string] $Step,
|
||||
[string] $Only = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path $PSScriptRoot -Parent
|
||||
$Proj = Join-Path $Root 'LifePlay.uproject'
|
||||
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$Tools = ($PSScriptRoot -replace '\\', '/')
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$cfg = Get-Content $Config -Raw | ConvertFrom-Json
|
||||
$backup = $cfg.backup
|
||||
$folder = $cfg.folder
|
||||
New-Item -ItemType Directory -Force $backup | Out-Null
|
||||
|
||||
function Invoke-UE([string[]] $Arguments, [string] $Filter) {
|
||||
$out = & $UE $Proj @Arguments -unattended -nopause -nosplash 2>&1 | ForEach-Object { "$_" }
|
||||
$out | Where-Object { $_ -match $Filter -and $_ -notmatch 'LogInit: Display' }
|
||||
}
|
||||
|
||||
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
|
||||
function Expand-One([string] $Archive, [string] $Dest) {
|
||||
# 7-Zip lit zip, rar et 7z (Pocolov 03 : source/Hair3.rar) ; Expand-Archive en secours pour les zip.
|
||||
if (Test-Path $SevenZip) { & $SevenZip x -y "-o$Dest" $Archive | Out-Null }
|
||||
else { Expand-Archive -LiteralPath $Archive -DestinationPath $Dest -Force }
|
||||
}
|
||||
|
||||
function Expand-All([string] $Archive, [string] $Dest) {
|
||||
Expand-One $Archive $Dest
|
||||
# Archives imbriquees (Pocolov : source/<nom>.zip ou .rar)
|
||||
for ($pass = 0; $pass -lt 3; $pass++) {
|
||||
$inner = Get-ChildItem $Dest -Recurse -Include *.zip, *.rar, *.7z | Where-Object { -not (Test-Path ($_.FullName + '.done')) }
|
||||
if (-not $inner) { break }
|
||||
foreach ($z in $inner) {
|
||||
Expand-One $z.FullName (Join-Path $z.DirectoryName ($z.BaseName -replace '\s', '_'))
|
||||
New-Item -ItemType File ($z.FullName + '.done') | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Statistiques d'une image : saturation moyenne, part de pixels tres sombres / tres clairs, part sombre du bas.
|
||||
function Get-ImageStats([string] $Path) {
|
||||
try { $bmp = [System.Drawing.Bitmap]::FromFile($Path) } catch { return $null }
|
||||
try {
|
||||
$step = [Math]::Max(1, [int]([Math]::Sqrt($bmp.Width * $bmp.Height / 20000)))
|
||||
$n = 0; $sat = 0.0; $dark = 0; $light = 0; $bottomDark = 0; $bottomN = 0; $sideDark = 0; $sideN = 0
|
||||
for ($y = 0; $y -lt $bmp.Height; $y += $step) {
|
||||
for ($x = 0; $x -lt $bmp.Width; $x += $step) {
|
||||
$c = $bmp.GetPixel($x, $y)
|
||||
$max = [Math]::Max($c.R, [Math]::Max($c.G, $c.B)); $min = [Math]::Min($c.R, [Math]::Min($c.G, $c.B))
|
||||
$sat += ($max - $min) / 255.0
|
||||
$l = ($c.R + $c.G + $c.B) / 765.0
|
||||
if ($l -lt 0.15) { $dark++ } elseif ($l -gt 0.85) { $light++ }
|
||||
if ($y -gt $bmp.Height * 0.8) { $bottomN++; if ($l -lt 0.3) { $bottomDark++ } }
|
||||
if ($x -lt $bmp.Width * 0.05 -or $x -ge $bmp.Width * 0.95) { $sideN++; if ($l -lt 0.3) { $sideDark++ } }
|
||||
$n++
|
||||
}
|
||||
}
|
||||
[pscustomobject]@{
|
||||
File = $Path; Width = $bmp.Width; Height = $bmp.Height
|
||||
Saturation = [Math]::Round($sat / $n, 3)
|
||||
Binary = [Math]::Round(($dark + $light) / $n, 3)
|
||||
Dark = [Math]::Round($dark / $n, 3)
|
||||
BottomDark = if ($bottomN) { [Math]::Round($bottomDark / $bottomN, 3) } else { 0 }
|
||||
SideDark = if ($sideN) { [Math]::Round($sideDark / $sideN, 3) } else { 0 }
|
||||
}
|
||||
} finally { $bmp.Dispose() }
|
||||
}
|
||||
|
||||
function Select-Textures($all, [string[]] $StrandSlots) {
|
||||
# Archive avec une tete complete (Pocolov 03 : peau, yeux, sourcils) : ne garder que les textures nommees "hair"
|
||||
# s'il y en a.
|
||||
$stats = @($all | Where-Object { (Split-Path $_.File -Leaf) -match 'hair' })
|
||||
if (-not $stats) { $stats = $all }
|
||||
# Masque : niveaux de gris, majoritairement noir/blanc. Masque des meches : vertical (hauteur > largeur ; un masque
|
||||
# carre est en general la lisiere de la calotte, Pocolov 16) ; a
|
||||
# egalite, celui dont le nom contient un emplacement des meches (Hair_01 -> Hair_01A1), puis l'ordre alphabetique
|
||||
# (A1 avant A2). Couleur : le reste ; de preference grise (teintee par HairColor), sinon la plus grande.
|
||||
$masks = $stats | Where-Object { $_.Saturation -lt 0.05 -and $_.Binary -gt 0.55 }
|
||||
$matchesSlot = { param($f) foreach ($s in $StrandSlots) { if ($s -and (Split-Path $f -Leaf) -match [regex]::Escape($s)) { return $true } }; $false }
|
||||
# Puis bords lateraux sombres (carte de meches a bords transparents : Hair_01A1 plutot que Hair_01A, Pocolov 03).
|
||||
$strands = $masks | Where-Object { $_.Height -gt $_.Width } | Sort-Object @{ e = { & $matchesSlot $_.File }; Descending = $true }, @{ e = { $_.SideDark -gt 0.5 }; Descending = $true }, @{ e = { Split-Path $_.File -Leaf } } | Select-Object -First 1
|
||||
$colors = $stats | Where-Object { $_ -notin $masks }
|
||||
$diffuse = $colors | Sort-Object @{ e = { $_.Saturation -lt 0.05 }; Descending = $true }, @{ e = { $_.Height * $_.Width }; Descending = $true } | Select-Object -First 1
|
||||
[pscustomobject]@{ Diffuse = $diffuse; Strands = $strands }
|
||||
}
|
||||
|
||||
$report = @()
|
||||
foreach ($h in $cfg.hairs) {
|
||||
if ($Only -and $h.name -ne $Only) { continue }
|
||||
$name = $h.name
|
||||
$dir = Join-Path $backup $name
|
||||
$meshPath = "$folder/$name"
|
||||
$work = "$folder/_Work_$name"
|
||||
|
||||
if ($Step -eq 'Inspect') {
|
||||
Write-Host "== $name : inspection"
|
||||
if (-not (Test-Path $h.archive)) { $report += "$name : archive absente ($($h.archive))"; continue }
|
||||
Remove-Item (Join-Path $dir 'src') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Expand-All $h.archive (Join-Path $dir 'src')
|
||||
$norm = Join-Path $dir 'norm'
|
||||
New-Item -ItemType Directory -Force $norm | Out-Null
|
||||
$fbx = Get-ChildItem (Join-Path $dir 'src') -Recurse -Filter *.fbx | Sort-Object Length -Descending | Select-Object -First 1
|
||||
if (-not $fbx) { $report += "$name : aucun FBX dans l'archive"; continue }
|
||||
Copy-Item $fbx.FullName (Join-Path $norm 'hair.fbx') -Force
|
||||
$i = 0; $stats = @()
|
||||
foreach ($t in Get-ChildItem (Join-Path $dir 'src') -Recurse -Include *.png, *.jpg, *.jpeg, *.tga, *.bmp) {
|
||||
$i++
|
||||
$copy = Join-Path $norm ("tex{0:D2}_{1}{2}" -f $i, ($t.BaseName -replace '[^A-Za-z0-9_-]', '_'), $t.Extension.ToLower())
|
||||
Copy-Item $t.FullName $copy -Force
|
||||
$s = Get-ImageStats $copy
|
||||
if ($s) { $stats += $s }
|
||||
}
|
||||
$json = Join-Path $dir 'import.json'
|
||||
$fbxArg = (Join-Path $norm 'hair.fbx') -replace '\\', '/'
|
||||
Invoke-UE @('-run=pythonscript', "-script=$Tools/HairImportFbx.py fbx=$fbxArg dest=$work report=$($json -replace '\\','/')") 'HAIRIMPORT' | Write-Host
|
||||
if (-not (Test-Path $json)) { $report += "$name : import du FBX echoue"; continue }
|
||||
$imp = Get-Content $json -Raw | ConvertFrom-Json
|
||||
$head = $imp.meshes | Where-Object { $_.name -match 'head' } | Sort-Object triangles -Descending | Select-Object -First 1
|
||||
$hairMeshes = @($imp.meshes | Where-Object { $_ -ne $head })
|
||||
$hair = $hairMeshes | Sort-Object triangles -Descending | Select-Object -First 1
|
||||
# Champs detectes : ceux valant "auto" a la premiere inspection (memorises dans auto_fields, recalcules a chaque
|
||||
# inspection) ; une valeur saisie a la main n'est jamais remplacee.
|
||||
if (-not $h.auto_fields) { $h | Add-Member auto_fields @('mesh', 'head', 'cap_slots', 'diffuse', 'strands_opacity' | Where-Object { $h.$_ -eq 'auto' }) -Force }
|
||||
$auto = { param($f) $f -in $h.auto_fields }
|
||||
if (& $auto 'mesh') { $h.mesh = $hair.path }
|
||||
if (& $auto 'head') { $h.head = if ($head) { $head.path } else { $hair.path } }
|
||||
# Associations materiau -> textures declarees dans le FBX (Tools/FbxMaterialMap.py), pour le rapport et le cas
|
||||
# d'un emplacement unique.
|
||||
$fbxMap = $null
|
||||
try { $fbxMap = (& python (Join-Path $PSScriptRoot 'FbxMaterialMap.py') (Join-Path $norm 'hair.fbx')) -join "`n" | ConvertFrom-Json } catch { }
|
||||
$findCopy = { param($fileName) if ($fileName) { $b = [IO.Path]::GetFileNameWithoutExtension($fileName) -replace '[^A-Za-z0-9_-]', '_'; ($stats | Where-Object { (Split-Path $_.File -Leaf) -match ('^tex\d+_' + [regex]::Escape($b) + '\.') } | Select-Object -First 1).File } }
|
||||
# Calotte : emplacement nomme base/cap/scalp, seulement s'il reste au moins un autre emplacement pour les meches.
|
||||
if (& $auto 'cap_slots') { $h.cap_slots = if ($hair.slots.Count -gt 1) { (@($hair.slots | Where-Object { $_ -match 'base|cap|scalp' }) -join ',') } else { '' } }
|
||||
$strandSlots = @($hair.slots | Where-Object { $_ -notin ($h.cap_slots -split ',') })
|
||||
$sel = Select-Textures $stats $strandSlots
|
||||
if (& $auto 'diffuse') { $h.diffuse = if ($sel.Diffuse) { $sel.Diffuse.File -replace '\\', '/' } else { '' } }
|
||||
if (& $auto 'strands_opacity') {
|
||||
# Emplacement unique (Pocolov 11) : masque declare par le FBX pour cet emplacement ; sinon masque vertical.
|
||||
$declared = if ($strandSlots.Count -eq 1 -and $hair.slots.Count -eq 1 -and $fbxMap) { & $findCopy $fbxMap.($strandSlots[0]).TransparentColor }
|
||||
$h.strands_opacity = if ($declared) { $declared -replace '\\', '/' } elseif ($sel.Strands) { $sel.Strands.File -replace '\\', '/' } else { '' }
|
||||
}
|
||||
$notes = @()
|
||||
if ($hairMeshes.Count -gt 1) { $notes += "$($hairMeshes.Count) maillages hors tete : seul '$($hair.name)' est converti" }
|
||||
if (-not $head) { $notes += 'pas de tete de reference : ajustement sur la coiffure elle-meme (margin 1.0)'; if (-not $h.margin) { $h | Add-Member margin '1.0' -Force } }
|
||||
if (-not $h.diffuse -or -not $h.strands_opacity) { $notes += 'textures non identifiees : renseigner diffuse / strands_opacity' }
|
||||
$h | Add-Member inspected $true -Force
|
||||
$report += "$name : coiffure $($hair.name) ($($hair.triangles) tri., emplacements $($hair.slots -join ',')), tete $(if ($head) { $head.name } else { '-' }), calotte [$($h.cap_slots)], couleur $(Split-Path $h.diffuse -Leaf), masque $(Split-Path $h.strands_opacity -Leaf)$(if ($notes) { ' ; ' + ($notes -join ' ; ') })"
|
||||
if ($fbxMap) { $report += ($fbxMap.PSObject.Properties | ForEach-Object { " FBX $($_.Name) : " + (($_.Value.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ', ') }) }
|
||||
$report += ($stats | ForEach-Object { " texture $(Split-Path $_.File -Leaf) : $($_.Width)x$($_.Height), saturation $($_.Saturation), noir/blanc $($_.Binary), bas sombre $($_.BottomDark), bords sombres $($_.SideDark)" })
|
||||
}
|
||||
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 }
|
||||
$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) } }
|
||||
$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
|
||||
if (-not $scaleLine) { $report += "$name : essai a blanc en echec"; continue }
|
||||
$sx = [double]$Matches[1]; $sy = [double]$Matches[2]
|
||||
if ($sx -lt 0.6 -or $sx -gt 1.8 -or $sy -lt 0.6 -or $sy -gt 1.8) { $report += "$name : echelle hors limites (X=$sx Y=$sy), sautee : verifier la tete de reference"; continue }
|
||||
$conv = Invoke-UE (@('-run=LPHairFromStaticMesh') + $common) 'LogLPHairFromStatic: (Warning|Error)'
|
||||
if (-not ($conv -match 'sauvegarde :.*' + [regex]::Escape($name) + '\.uasset')) { $conv | Write-Host; $report += "$name : conversion en echec"; continue }
|
||||
$placed = ($conv | Where-Object { $_ -match 'bornes' }) -replace '.*bornes ', ''
|
||||
$mat = Invoke-UE @('-run=pythonscript', "-script=$Tools/HairMaterials.py mesh=$meshPath diffuse=$($h.diffuse) strands_opacity=$($h.strands_opacity) cap_opacity=$($h.cap_opacity) cap=$($h.cap_slots) thicken=$($h.thicken)") 'HAIRMAT'
|
||||
$mat | Write-Host
|
||||
$depOk = [bool]($mat -match 'dependances.*-> OK')
|
||||
# Nettoyage : dossier de travail deplace vers la sauvegarde.
|
||||
$workDir = Join-Path $Root ("Content" + $work.Substring(5))
|
||||
if (Test-Path $workDir) { Move-Item $workDir (Join-Path $dir 'work') -Force }
|
||||
# Declaration et credits.
|
||||
$lphairDir = Join-Path $Root "Content\Modules\$($cfg.module)\Hairstyles"
|
||||
New-Item -ItemType Directory -Force $lphairDir | Out-Null
|
||||
$credit = "$($h.title) by $($h.author), $($h.license), $($h.url)"
|
||||
$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.',
|
||||
"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
|
||||
}
|
||||
$report += "$name : OK, echelle X=$sx Y=$sy, bornes $placed, dependances $(if ($depOk) { 'OK' } else { 'A VERIFIER' })"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Step -eq 'Inspect') {
|
||||
$cfg | ConvertTo-Json -Depth 6 | Set-Content $Config -Encoding utf8
|
||||
}
|
||||
else {
|
||||
Invoke-UE @('-run=pythonscript', "-script=$Tools/BuildCollections.py") 'collection\(s\) ecrite' | Write-Host
|
||||
}
|
||||
$reportFile = Join-Path $backup "rapport_$Step.txt"
|
||||
$report | Set-Content $reportFile -Encoding utf8
|
||||
Write-Host "`n=== Bilan ($Step) : $reportFile"
|
||||
$report | Write-Host
|
||||
127
Tools/hairs/HAIRS1.json
Normal file
127
Tools/hairs/HAIRS1.json
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
{
|
||||
"module": "lp_Revival",
|
||||
"folder": "/Game/Heavy/New5/Hairs",
|
||||
"backup": "D:/Game/LifePlay_Backup_5.5/HAIRS1",
|
||||
"hairs": [
|
||||
{
|
||||
"name": "Pocolov03",
|
||||
"archive": "D:/Game/pocolov-hair-03.zip",
|
||||
"title": "Pocolov Hair 03",
|
||||
"url": "https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov03/Box001",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov03/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov03/norm/tex10_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov03/norm/tex12_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov06",
|
||||
"archive": "D:/Game/pocolov-hair-06.zip",
|
||||
"title": "Pocolov Hair 06",
|
||||
"url": "https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov06/Hair_06",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov06/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov06/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov06/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov11",
|
||||
"archive": "D:/Game/pocolov-hair-11.zip",
|
||||
"title": "Pocolov Hair 11",
|
||||
"url": "https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov11/Hair_11",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov11/Head",
|
||||
"cap_slots": "",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov11/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov11/norm/tex04_Hair_Texture_A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov15",
|
||||
"archive": "D:/Game/pocolov-hair-15.zip",
|
||||
"title": "Pocolov Hair 15",
|
||||
"url": "https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov15/Hair_15",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov15/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov15/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov15/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov16",
|
||||
"archive": "D:/Game/pocolov-hair-16.zip",
|
||||
"title": "Pocolov Hair 16",
|
||||
"url": "https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov16/Hair_16",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov16/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov16/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov16/norm/tex04_Hair_01A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue