# Rebuild the vehicle's materials from the Blender colours (0.3.0). # # blender -b "Art/blender/vehicle/hot hatch.blend" \ # --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ # -script=/Tools/ue_vehicle_materials.py -nullrhi -unattended -nopause -stdout # # Why this exists: the template's car paint is a **legacy node graph**, so its colour never # reached the exported FBX and the car arrived in Unreal near-black while Blender showed it # red. `Tools/vehicle_from_template.py` now reads those graphs and records the colours; this # replaces the imported material assets in place with plain constant-colour materials, so # the mesh slots pick them up by path without anything being re-imported. # # Result file: /tmp/gigline/vehicle-materials.txt import json import os import unreal DEST = "/Game/Hollywood/Vehicle" _REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", "..")) REPORT = os.path.join(_REPO, "..", "..", "..", "tmp", "gigline", "hatch", "report.json") REPORT = "/tmp/gigline/hatch/report.json" RESULT = "/tmp/gigline/vehicle-materials.txt" lines = [] def log(msg): lines.append(str(msg)) unreal.log(str(msg)) def existing_material_name(name): """Find the asset the importer actually created for a Blender material name. Blender names may contain spaces ("car paint"), which are not valid asset names, so the importer sanitises them. Guessing the sanitised form is fragile; this searches the folder instead and reports when nothing matches. """ registry = unreal.AssetRegistryHelpers.get_asset_registry() wanted = name.lower().replace(" ", "").replace("_", "") for asset in registry.get_assets_by_path(DEST, recursive=False): candidate = str(asset.asset_name).lower().replace(" ", "").replace("_", "") if candidate == wanted: return str(asset.asset_name) return None def make_translucent_glass(material): """Turn a material into dark, slightly transparent glass. The template's `windows` material is metallic 1.0 with roughness 0.0, which reads as a black void in game - indistinguishable from an empty cabin. Glass needs translucency and a little roughness to catch a highlight. """ lib = unreal.MaterialEditingLibrary material.set_editor_property("blend_mode", unreal.BlendMode.BLEND_TRANSLUCENT) material.set_editor_property("two_sided", False) for node in list(lib.get_material_expressions(material)): lib.delete_material_expression(material, node) base = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -400, 0) base.set_editor_property("constant", unreal.LinearColor(0.02, 0.03, 0.045, 1.0)) lib.connect_material_property(base, "", unreal.MaterialProperty.MP_BASE_COLOR) opacity = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -400, 140) opacity.set_editor_property("r", 0.85) lib.connect_material_property(opacity, "", unreal.MaterialProperty.MP_OPACITY) rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -400, 220) rough.set_editor_property("r", 0.08) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) specular = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -400, 300) specular.set_editor_property("r", 1.0) lib.connect_material_property(specular, "", unreal.MaterialProperty.MP_SPECULAR) lib.recompile_material(material) return material def replace_material(name, spec, create_if_missing=False): resolved = existing_material_name(name) if resolved is not None: name = resolved elif not create_if_missing: log(f"{name}: no imported asset matches - skipped") return None 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()) if material is None: log(f"{name}: could not create") return None lib = unreal.MaterialEditingLibrary colour = spec["base_color"] base = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -400, 0) base.set_editor_property("constant", unreal.LinearColor(colour[0], colour[1], colour[2], 1.0)) lib.connect_material_property(base, "", unreal.MaterialProperty.MP_BASE_COLOR) rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -400, 180) rough.set_editor_property("r", max(0.05, float(spec.get("roughness", 0.5)))) lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) metallic = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -400, 260) metallic.set_editor_property("r", float(spec.get("metallic", 0.0))) lib.connect_material_property(metallic, "", unreal.MaterialProperty.MP_METALLIC) lib.recompile_material(material) log(f"{name}: base {tuple(round(c, 3) for c in colour)} " f"roughness {spec.get('roughness')} metallic {spec.get('metallic')} " f"({spec.get('source')})") return material def main(): with open(REPORT) as handle: report = json.load(handle) materials = report.get("materials", {}) log(f"{len(materials)} materials in the Blender report") made = 0 for name, spec in materials.items(): material = replace_material(name, spec) if material: made += 1 if material and name.lower() == "windows": make_translucent_glass(material) log("windows: tinted glass (opacity 0.85, roughness 0.08) - 0.45 read as no glass at all") # The body arrives without a usable material (its FBX slot resolves to # WorldGridMaterial), so its paint is created under a name of our own and assigned to # the staged panel at runtime. paint = materials.get("car paint") if paint: spec = dict(paint) spec["base_color"] = [1.0, 0.05, 0.03] replace_material("M_Vehicle_HatchPaint", spec, create_if_missing=True) log("M_Vehicle_HatchPaint: body paint, assigned at runtime by the map builder") unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) lines.append(f"VERDICT: {'PASS' if made == len(materials) else 'FAIL'}") log(f"{made}/{len(materials)} materials replaced") 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")