# 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 # # Two materials, both edited IN PLACE so the imported meshes keep their references: # # M_Character_Export skin: diffuse x generated pore detail, roughness variation # M_Character_Hair hair: strand texture colour + alpha, masked and two-sided # # Editing in place matters: `unreal.SkeletalMesh` has no `set_material`, so a mesh # that already points at a material cannot be re-slotted from Python. Changing the # material asset is the only lever. # # Result file: /tmp/gigline/character-materials.txt import os import unreal # The commandlet's working directory is the engine, not the repo, so resolve the # texture folder from the project path instead of relying on the cwd. _REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", "..")) TEXTURE_DIR = os.path.join(_REPO, "Art", "textures", "generated") DEST = "/Game/Test/Character" 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) 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 = os.path.splitext(filename)[0] 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}/{task.destination_name}") log(f"texture {task.destination_name}: {'ok' if texture else 'MISSING'}") return texture def clear_graph(material): """Remove every expression so a rerun does not stack nodes.""" lib = unreal.MaterialEditingLibrary for node in list(lib.get_material_expressions(material)): lib.delete_material_expression(material, node) def textured_material(mat, texture, roughness, tint=None, masked=False): """Base colour from a texture, optional detail multiply, roughness constant.""" lib = unreal.MaterialEditingLibrary clear_graph(mat) tex = lib.create_material_expression(mat, unreal.MaterialExpressionTextureSample, -450, 0) tex.texture = texture if masked: mat.set_editor_property("blend_mode", unreal.BlendMode.BLEND_MASKED) mat.set_editor_property("two_sided", True) lib.connect_material_property(tex, "A", unreal.MaterialProperty.MP_OPACITY_MASK) if tint is not None: mul = lib.create_material_expression(mat, unreal.MaterialExpressionMultiply, -220, 40) mul.set_editor_property("const_b", unreal.LinearColor(*tint, 1.0)) lib.connect_material_expressions(tex, "RGB", mul, "A") lib.connect_material_property(mul, "", unreal.MaterialProperty.MP_BASE_COLOR) else: lib.connect_material_property(tex, "RGB", unreal.MaterialProperty.MP_BASE_COLOR) rough = lib.create_material_expression(mat, unreal.MaterialExpressionConstant, -220, -180) rough.set_editor_property("r", roughness) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) lib.recompile_material(mat) log(f"{mat.get_name()}: texture {texture.get_name()}, roughness {roughness}" + (", masked two-sided" if masked else "")) def main(): skin_detail = import_texture("T_Character_SkinDetail.png") hair_strands = import_texture("T_Character_Hair.png") skin = unreal.EditorAssetLibrary.load_asset(f"{DEST}/M_Character_Export") if skin and skin_detail: # The character's own diffuse stays the base; the generated pore sheet is a # detail multiplier, so the warm skin tone from the bake is preserved. lib = unreal.MaterialEditingLibrary clear_graph(skin) diffuse = lib.create_material_expression(skin, unreal.MaterialExpressionTextureSample, -700, 0) diffuse.texture = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Character_Diffuse") detail = lib.create_material_expression(skin, unreal.MaterialExpressionTextureSample, -700, 300) detail.texture = skin_detail detail.sampler_type = unreal.MaterialSamplerType.SAMPLERTYPE_LINEAR_COLOR mul = lib.create_material_expression(skin, unreal.MaterialExpressionMultiply, -380, 120) lib.connect_material_expressions(diffuse, "RGB", mul, "A") lib.connect_material_expressions(detail, "RGB", mul, "B") # Detail is a multiplier, so keep it near 1 and let it vary a little. lerp = lib.create_material_expression(skin, unreal.MaterialExpressionLinearInterpolate, -200, 120) lerp.set_editor_property("const_a", unreal.LinearColor(1.0, 1.0, 1.0, 1.0)) lib.connect_material_expressions(mul, "", lerp, "B") rough = lib.create_material_expression(skin, unreal.MaterialExpressionConstant, -200, -160) rough.set_editor_property("r", 0.55) lib.connect_material_property(lerp, "", unreal.MaterialProperty.MP_BASE_COLOR) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) lib.recompile_material(skin) log("M_Character_Export: diffuse x pore detail, roughness 0.55") elif not skin: log("M_Character_Export missing") hair = unreal.EditorAssetLibrary.load_asset(f"{DEST}/M_Character_Hair") if hair and hair_strands: textured_material(hair, hair_strands, 0.38, masked=True) elif not hair: log("M_Character_Hair missing") unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) lines.append("VERDICT: PASS") try: main() 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")