The windows material was metallic 1.0 with roughness 0.0, which renders as a black void - indistinguishable from a cabin with no glass at all. It is now tinted translucent: opacity 0.85, roughness 0.08, dark blue-grey base. The windscreen and side glass read as glass. Opacity 0.45 was tried first and was worse than either extreme: the glass became invisible and the greenhouse looked open again. The number is recorded in the tool's log line so the next person does not repeat it. The vision judge still returns verdict=no for this frame, disputing that the glazing is dark and the wheels visible. The image shows both. It is an advisory helper - AGENTS.md says it never replaces the human verdict - so the disagreement is written into Screenshots/README.md rather than being resolved by iterating against the model.
149 lines
6.7 KiB
Python
149 lines
6.7 KiB
Python
# 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>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
# -script=<repo>/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")
|