- 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>
104 lines
6.1 KiB
Python
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"))
|