LifePlay-Revival/Tools/HairMaterials.py
edujiro5972 a680f0bf19 H3b : outillage des mods d'assets (BuildMod, ModSources, pipeline en mode mod)
- Tools/BuildMod.ps1 : controles, cuisson DLC contre la version de reference, controle du
  conteneur, archive installable testee, installation optionnelle dans un jeu de test.
- Tools/ModSource.ps1 : sources dans ModSources/<Nom>, montees dans Mods/ le temps d'un
  travail ; le build du jeu de base ne contient rien des mods.
- HairPipeline en mode mod ; HairMaterials : analyse du dossier (plantage dans un plugin),
  reutilisation des textures ; sous-systeme : repli DoesPackageExist.

Valide en jeu le 2026-10-03 (CHG-104).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:06:48 +02:00

104 lines
6.1 KiB
Python

# Protocole de conversion d'une coiffure (Docs/Migration/13_Protocole_Coiffure.md), etape 4 : textures et materiaux.
#
# Importe les textures de la coiffure, cree deux materiaux enfants de /Game/Heavy/Daz/Bridget/Hairs/DazHair (le
# materiau des coiffures Daz : parametres HairColor et HairThickness pilotes par le jeu) et les affecte aux
# emplacements du SkeletalMesh produit par LPHairFromStaticMesh :
# <Nom>_Cap : emplacements listes dans cap= (calotte) ; opacite cap_opacity= ou "white" (calotte opaque)
# <Nom>_Strands : tous les autres emplacements (meches) ; opacite strands_opacity=
# Controle final : dependances du SkeletalMesh (squelette et ces deux materiaux seulement).
#
# Usage :
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<projet>/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/Nom
# diffuse=<couleur.png> strands_opacity=<masque.jpg> cap_opacity=white cap=Hair_Base [thicken=1]" -unattended -nopause -nosplash
# Les fichiers de textures sont des chemins sur disque ; les textures sont creees a cote du SkeletalMesh.
# Sortie : lignes "HAIRMAT" du journal.
import os
import sys
import unreal
eal = unreal.EditorAssetLibrary
mel = unreal.MaterialEditingLibrary
tools = unreal.AssetToolsHelpers.get_asset_tools()
args = dict(a.split("=", 1) for a in sys.argv[1:] if "=" in a)
def import_texture(file, folder, name):
# Texture deja importee depuis le meme fichier : reutilisee. Une autre source dans un plugin (mod, H3) : refus,
# HairPipeline -Rebuild deplace d'abord les textures d'un mod vers la sauvegarde.
path = folder + "/" + name
if eal.does_asset_exist(path):
existing = unreal.load_asset(path)
data = existing.get_editor_property("asset_import_data")
source = data.get_first_filename().replace("\\", "/") if data else ""
if source.lower() == file.replace("\\", "/").lower():
unreal.log_warning("HAIRMAT texture %s reutilisee (meme source)" % name)
return existing
if not folder.startswith("/Game/"):
raise RuntimeError("HAIRMAT texture %s existe deja avec une autre source (%s) : relancer HairPipeline avec -Rebuild" % (path, source))
task = unreal.AssetImportTask()
task.filename = file.replace("\\", "/")
task.destination_path = folder
task.destination_name = name
task.automated = True
task.replace_existing = True
task.save = True
tools.import_asset_tasks([task])
return unreal.load_asset(folder + "/" + name)
def make_mic(folder, name, parent, diffuse, opacity, thicken):
path = folder + "/" + name
mic = unreal.load_asset(path) if eal.does_asset_exist(path) else tools.create_asset(
name, folder, unreal.MaterialInstanceConstant, unreal.MaterialInstanceConstantFactoryNew())
mel.set_material_instance_parent(mic, parent)
mel.set_material_instance_texture_parameter_value(mic, "MapDiffuse", diffuse)
mel.set_material_instance_texture_parameter_value(mic, "MapOpacity", opacity)
mel.set_material_instance_scalar_parameter_value(mic, "Thicken", thicken)
mel.update_material_instance(mic)
eal.save_loaded_asset(mic, only_if_is_dirty=False)
return mic
mesh_path = args.get("mesh")
if not mesh_path or not args.get("diffuse") or not args.get("strands_opacity"):
unreal.log_error("HAIRMAT usage : mesh=/Game/... diffuse=<fichier> strands_opacity=<fichier> [cap_opacity=<fichier>|white] [cap=Slot1,Slot2] [thicken=1]")
else:
folder, name = mesh_path.rsplit("/", 1)
# Dans un plugin (mod, H3), le registre de la commandlet ne connait pas encore les assets du dossier : sans cette
# analyse, does_asset_exist repond faux, la creation d'un asset existant ouvre une boite de dialogue et plante.
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([folder], True)
thicken = float(args.get("thicken", "1"))
cap_slots = [s for s in args.get("cap", "").split(",") if s]
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")
strands = make_mic(folder, name + "_Strands", parent, diffuse, strands_opacity, thicken)
# 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)
registry = unreal.AssetRegistryHelpers.get_asset_registry()
# Chemin de paquet (et non de fichier) : vaut aussi pour un plugin de mod (/<Mod>/..., H3).
registry.scan_paths_synchronous([folder], force_rescan=True)
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"} | used
unreal.log_warning("HAIRMAT dependances de %s : %s -> %s" % (name, deps, "OK" if set(deps) == expected else "A VERIFIER"))