# Wire the generated character textures into Unreal materials (0.2.2). # # python3 Tools/generate_character_textures.py --out Art/textures/generated # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ # -script=/Tools/ue_character_materials.py -nullrhi -unattended -nopause -stdout # # Why new materials instead of editing the imported ones: # # * `unreal.MaterialEditingLibrary` SEGFAULTS in `-run=pythonscript` when it clears # and rebuilds an existing material graph. The first version of this tool did that # and crashed the commandlet with no traceback. # * `unreal.SkeletalMesh` has no `set_material`, so a mesh slot cannot be repointed # from Python. The pawn therefore assigns the skin material at runtime (see # AGigLineCharacter) and the hair's existing MaterialInstanceConstant gets a new # parent, which is a property write rather than a graph edit. # # Assets created: # # M_Character_Skin diffuse x generated pore detail, roughness 0.55 # M_Character_Hair_Mat strand sheet colour + alpha, masked and two-sided # # Result file: /tmp/gigline/character-materials.txt import os import unreal DEST = "/Game/Test/Character" _REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", "..")) TEXTURE_DIR = os.path.join(_REPO, "Art", "textures", "generated") RESULT = "/tmp/gigline/character-materials.txt" lines = [] def log(msg): lines.append(str(msg)) unreal.log(str(msg)) def import_texture(filename): path = os.path.join(TEXTURE_DIR, filename) name = os.path.splitext(filename)[0] existing = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}") if not os.path.exists(path): log(f"missing {path}") return None task = unreal.AssetImportTask() task.filename = path task.destination_path = DEST task.destination_name = name task.automated = True task.save = True task.replace_existing = True unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) texture = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}") log(f"texture {name}: {'ok' if texture else 'MISSING'}" + ("" if existing is None else " (replaced)")) return texture def new_material(name): """Create an empty material, replacing one from a previous run.""" full = f"{DEST}/{name}" if unreal.EditorAssetLibrary.does_asset_exist(full): unreal.EditorAssetLibrary.delete_asset(full) tools = unreal.AssetToolsHelpers.get_asset_tools() material = tools.create_asset(name, DEST, unreal.Material, unreal.MaterialFactoryNew()) # Without this flag Unreal refuses to use the material on a skeletal mesh and # silently falls back to the default material, which renders near-black here. material.set_editor_property("used_with_skeletal_mesh", True) return material def skin_material(diffuse, detail, unlit_test=False): material = new_material("M_Character_Skin") lib = unreal.MaterialEditingLibrary # Base colour is a constant, not the baked diffuse. Sampling T_Character_Diffuse # rendered the body black: the MPFB mesh's UVs do not map onto that bake, so every # lookup fell outside the texture. A constant tone is correct-looking and honest; # the pore sheet waits until the UVs are verified. base = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -420, 0) # Measured, not guessed: the red test colour reached the render, so the assignment # was always working - the skin simply reads dark under the demo's key light. This # tone is lifted to sit in shadow the way the blueprint's skin does. base.set_editor_property("constant", unreal.LinearColor(0.78, 0.55, 0.44, 1.0)) if unlit_test: # Diagnostic: unlit renders the constant exactly, so the render tells us whether # a wrong skin tone comes from lighting/shading or from the material itself. material.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_UNLIT) lib.connect_material_property(base, "", unreal.MaterialProperty.MP_EMISSIVE_COLOR) else: lib.connect_material_property(base, "", unreal.MaterialProperty.MP_BASE_COLOR) rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -200, -200) rough.set_editor_property("r", 0.58) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) lib.recompile_material(material) log(("M_Character_Skin: UNLIT TEST, emissive " if unlit_test else "M_Character_Skin: constant skin tone, roughness 0.58 ") "(baked diffuse not used - it samples black on this mesh)") return material def hair_material(strands): material = new_material("M_Character_Hair_Mat") lib = unreal.MaterialEditingLibrary material.set_editor_property("blend_mode", unreal.BlendMode.BLEND_MASKED) material.set_editor_property("two_sided", True) sample = lib.create_material_expression(material, unreal.MaterialExpressionTextureSample, -520, 0) sample.texture = strands # Alpha only: the strand sheet's RGB is a pale highlight map and using it as the # base colour made the hair read blond and thin. lib.connect_material_property(sample, "A", unreal.MaterialProperty.MP_OPACITY_MASK) colour = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -520, 220) colour.set_editor_property("constant", unreal.LinearColor(0.030, 0.020, 0.014, 1.0)) lib.connect_material_property(colour, "", unreal.MaterialProperty.MP_BASE_COLOR) rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -220, -180) rough.set_editor_property("r", 0.40) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) lib.recompile_material(material) log("M_Character_Hair_Mat: dark constant colour, strand sheet as opacity mask, " "masked two-sided, roughness 0.40") return material def main(unlit_test=False): diffuse = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Character_Diffuse") log(f"diffuse: {'ok' if diffuse else 'MISSING'}") detail = import_texture("T_Character_SkinDetail.png") strands = import_texture("T_Character_Hair.png") if diffuse and detail: skin_material(diffuse, detail, unlit_test=unlit_test) if strands: hair = hair_material(strands) # Repoint the imported instance rather than editing its graph. instance = unreal.EditorAssetLibrary.load_asset(f"{DEST}/M_Character_Hair") if isinstance(instance, unreal.MaterialInstanceConstant): instance.set_editor_property("parent", hair) unreal.MaterialEditingLibrary.update_material_instance(instance) log("M_Character_Hair instance: parent -> M_Character_Hair_Mat") else: log(f"M_Character_Hair is {instance.get_class().get_name() if instance else 'missing'}, " "not an instance - skipped") unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) lines.append("VERDICT: PASS") try: # Environment, not a command-line flag: the commandlet drops unknown switches, so # the flag never reached Python. An env var is inherited. main(unlit_test=os.environ.get("GIGLINE_UNLIT_TEST") == "1") except Exception as exc: # noqa: BLE001 import traceback log(f"FAILED: {type(exc).__name__}: {exc}") log(traceback.format_exc()) lines.append("VERDICT: FAIL") with open(RESULT, "w") as f: f.write("\n".join(lines) + "\n")