The skin was never grey. A red test colour reached the render and the slot was confirmed holding M_Character_Skin, so the material and the assignment were both fine - the body was simply standing in building shadow and lit almost entirely by the blue skylight (intensity 2.6). Warm skin under blue fill reads blue-grey. Raising the sun from pitch -62 to -74, same camera: face (99, 98, 100) -> (131, 121, 117) arms (67, 74, 81) -> (71, 78, 85) This touches every approved shot, so it sits on the branch for the owner's approval rather than being folded in. Also records that the Unreal commandlet silently drops unknown switches, which is why a --unlit-test flag never took effect.
165 lines
7.4 KiB
Python
165 lines
7.4 KiB
Python
# Wire the generated character textures into Unreal materials (0.2.2).
|
|
#
|
|
# python3 Tools/generate_character_textures.py --out Art/textures/generated
|
|
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
# -script=<repo>/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")
|