diff --git a/Game/GigLine/Content/Hollywood/Vehicle/black_metal.uasset b/Game/GigLine/Content/Hollywood/Vehicle/black_metal.uasset
index 398cb84..123b1b7 100644
--- a/Game/GigLine/Content/Hollywood/Vehicle/black_metal.uasset
+++ b/Game/GigLine/Content/Hollywood/Vehicle/black_metal.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0818338ad583276d9fa0a64aa82028d2c054140ca9b0f6dcf3c2fb4a22948d2d
-size 56169
+oid sha256:8183bd1c290808cecc469ba3781bcb120f2ad36b3f08e1c2c07b2e70c19c4f55
+size 5696
diff --git a/Game/GigLine/Content/Hollywood/Vehicle/brake_caliper.uasset b/Game/GigLine/Content/Hollywood/Vehicle/brake_caliper.uasset
index 470689f..b0a3cb5 100644
--- a/Game/GigLine/Content/Hollywood/Vehicle/brake_caliper.uasset
+++ b/Game/GigLine/Content/Hollywood/Vehicle/brake_caliper.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e6c800341a8580c2c977681f7af4154bdef3b19ef11e444d30f5e741c222f9c4
-size 56201
+oid sha256:5b5dc2e7e92947d72d91cf4218f0144c96ce380cc0004e9c96bea86612301c3b
+size 5677
diff --git a/Game/GigLine/Content/Hollywood/Vehicle/plastic_black.uasset b/Game/GigLine/Content/Hollywood/Vehicle/plastic_black.uasset
index 03f2170..454ec22 100644
--- a/Game/GigLine/Content/Hollywood/Vehicle/plastic_black.uasset
+++ b/Game/GigLine/Content/Hollywood/Vehicle/plastic_black.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7de690fff9efda596b4e5dae71a19a52aff8fed5d9033669587668247c7d3ba0
-size 56195
+oid sha256:0b1e1d68e7e6454642b9cde51fdd27d97fe4fd61a2b4588556b76ef9e50e7592
+size 5677
diff --git a/Game/GigLine/Content/Hollywood/Vehicle/pure_black.uasset b/Game/GigLine/Content/Hollywood/Vehicle/pure_black.uasset
index b65a6f4..32f4bbc 100644
--- a/Game/GigLine/Content/Hollywood/Vehicle/pure_black.uasset
+++ b/Game/GigLine/Content/Hollywood/Vehicle/pure_black.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b9d0fac0b3cbe13c6806ec256169e1ff45d2b7164f467a4de335fecb5749d6c9
-size 56147
+oid sha256:603d4b5fbe4b7d1c60f6a5cc7ae24cb3902e584c0add28e7a9597c6d011d22ca
+size 5662
diff --git a/Game/GigLine/Content/Hollywood/Vehicle/rubber.uasset b/Game/GigLine/Content/Hollywood/Vehicle/rubber.uasset
index d51cd23..d1d5383 100644
--- a/Game/GigLine/Content/Hollywood/Vehicle/rubber.uasset
+++ b/Game/GigLine/Content/Hollywood/Vehicle/rubber.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0cfaa02a65d2dff743d9e36c6c4f2dcebc9a26cf51f72b72d138c8425acdefa6
-size 56089
+oid sha256:1c3f71638dd90b0933ed297a37ca2b3ab9a66422ce47f33eaa6093f15c6537dc
+size 5642
diff --git a/Game/GigLine/Content/Hollywood/Vehicle/windows.uasset b/Game/GigLine/Content/Hollywood/Vehicle/windows.uasset
new file mode 100644
index 0000000..81bcf76
--- /dev/null
+++ b/Game/GigLine/Content/Hollywood/Vehicle/windows.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cbd59ae1a1dc7f4ab6dc2fcf4342de7f65ba6bdf4b184fb4c6c7d32890c50f1d
+size 5676
diff --git a/Screenshots/COMPARISON.md b/Screenshots/COMPARISON.md
deleted file mode 100644
index 1aee619..0000000
--- a/Screenshots/COMPARISON.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Blender vs in-game
-
-Generated by `Tools/build_comparison_md.py` — do not edit by hand.
-
-- In-game captures: `Screenshots/demo/` (9 files)
-- Blender renders: `Screenshots/blender/` (22 files), made by `Tools/blender_screenshots.py`
-
-Left is what the engine draws, right is what Blender has. A gap between them is the import problem, not a taste problem: scale, material colour, missing parts and split systems all show up here before they show up in a review.
-
-## Side by side
-
-| In-game | Blender | Shot |
-| --- | --- | --- |
-|
| — | `review-demo-crossing.png` |
-|
| — | `review-demo-north-buildings.png` |
-|
| — | `review-demo-retail-frontage.png` |
-|
| — | `review-demo-spawn-view.png` |
-|
| — | `review-demo-street-level.png` |
-|
|
| `review-demo-third-person-front.png` |
-|
|
| `review-demo-third-person-walk.png` |
-|
|
| `review-demo-third-person.png` |
-|
|
| `review-demo-vehicle.png` |
-
-4 of 9 in-game captures have a Blender counterpart; the rest are street and building shots whose meshes come from the Geo pipeline rather than a .blend.
-
-## Signoff
-
-One row per in-game screen. `—` in Blender means there is nothing to compare against.
-
-| # | In-game screen | Blender counterpart | Verdict | Notes |
-| --- | --- | --- | --- | --- |
-| 1 | `review-demo-crossing.png` | — | ☐ | |
-| 2 | `review-demo-north-buildings.png` | — | ☐ | |
-| 3 | `review-demo-retail-frontage.png` | — | ☐ | |
-| 4 | `review-demo-spawn-view.png` | — | ☐ | |
-| 5 | `review-demo-street-level.png` | — | ☐ | |
-| 6 | `review-demo-third-person-front.png` | `character-angle-035.png` | ☐ | |
-| 7 | `review-demo-third-person-walk.png` | `character-angle-035.png` | ☐ | |
-| 8 | `review-demo-third-person.png` | `character-angle-035.png` | ☐ | |
-| 9 | `review-demo-vehicle.png` | `vehicle-angle-035.png` | ☐ | |
-
-Verdicts: ☐ pending · ☑ approved · ☒ rejected. The authoritative per-shot review stays in [README.md](README.md); this page exists to compare Blender against the engine.
diff --git a/Screenshots/README.md b/Screenshots/README.md
index 3e2d684..400ee95 100644
--- a/Screenshots/README.md
+++ b/Screenshots/README.md
@@ -196,3 +196,50 @@ Resolved and removed from this list on 2026-11-09:
- "The El Capitan Theatre has no marquee or blade sign" — `Tools/ue_stage_theatre_trim.py` stages all six trim actors; verified in `/tmp/gigline/theatre-import.txt`.
- "Materials are not yet created in Blender or fully integrated" — contradicted by the scene conventions above: every surface carries a look-dev material built in `Tools/ue_build_demo_map.py`.
+
+
+
+## Blender vs in-game
+
+Regenerated by `Tools/build_comparison_md.py`; everything between the markers is rewritten, so edit outside them.
+
+- In-game captures: `Screenshots/demo/` (9 files)
+- Blender renders: `Screenshots/blender/` (22 files), made by `Tools/blender_screenshots.py`
+
+Left is what the engine draws, right is what Blender has. A gap between them is the import problem, not a taste problem: scale, material colour, missing parts and split systems all show up here before they show up in a review.
+
+### Side by side
+
+| In-game | Blender | Shot |
+| --- | --- | --- |
+|
| — | `review-demo-crossing.png` |
+|
| — | `review-demo-north-buildings.png` |
+|
| — | `review-demo-retail-frontage.png` |
+|
| — | `review-demo-spawn-view.png` |
+|
| — | `review-demo-street-level.png` |
+|
|
| `review-demo-third-person-front.png` |
+|
|
| `review-demo-third-person-walk.png` |
+|
|
| `review-demo-third-person.png` |
+|
|
| `review-demo-vehicle.png` |
+
+4 of 9 in-game captures have a Blender counterpart; the rest are street and building shots whose meshes come from the Geo pipeline rather than a .blend.
+
+### Signoff
+
+One row per in-game screen. `—` in Blender means there is nothing to compare against.
+
+| # | In-game screen | Blender counterpart | Verdict | Notes |
+| --- | --- | --- | --- | --- |
+| 1 | `review-demo-crossing.png` | — | ☐ | |
+| 2 | `review-demo-north-buildings.png` | — | ☐ | |
+| 3 | `review-demo-retail-frontage.png` | — | ☐ | |
+| 4 | `review-demo-spawn-view.png` | — | ☐ | |
+| 5 | `review-demo-street-level.png` | — | ☐ | |
+| 6 | `review-demo-third-person-front.png` | `character-angle-035.png` | ☐ | |
+| 7 | `review-demo-third-person-walk.png` | `character-angle-035.png` | ☐ | |
+| 8 | `review-demo-third-person.png` | `character-angle-035.png` | ☐ | |
+| 9 | `review-demo-vehicle.png` | `vehicle-angle-035.png` | ☐ | |
+
+Verdicts: ☐ pending · ☑ approved · ☒ rejected. The authoritative per-shot review stays in [README.md](README.md); this page exists to compare Blender against the engine.
+
+
diff --git a/Tools/README.md b/Tools/README.md
index 940982b..9bb74f9 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -98,7 +98,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| `ue_vehicle_import.py` | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` |
| `vehicle_from_template.py` | Converts the owner's `hot hatch.blend` into UE-ready assets: normalises the scale to a documented 4.05 m, splits systems (body/trim/glass/lights/wheels), adds a `UCX_` hull, decimates two LODs, exports FBX per system and writes a report. | `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch` |
| `blender_screenshots.py` | Renders review screenshots out of a .blend: one per mesh object (`--mode objects`) and four turntable angles (`--mode scene`), framed from the scene's own bounds, into `Screenshots/blender/`. | `--label`, `--mode`, `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/blender_screenshots.py -- --label vehicle --mode both` |
-| `build_comparison_md.py` | Writes `Screenshots/COMPARISON.md`: in-game captures beside their Blender renders in one table, plus a signoff table with one row per in-game screen. Pairs by name with an alias map for camera-named shots. | (no arguments) | `python3 Tools/build_comparison_md.py` |
+| `build_comparison_md.py` | Regenerates the `Blender vs in-game` section of `Screenshots/README.md` between `` markers: in-game captures beside their Blender renders in one table, plus a signoff table with one row per in-game screen. Pairs by name with an alias map for camera-named shots. Run it after every capture or Blender render. | (no arguments) | `python3 Tools/build_comparison_md.py` |
## Tests
diff --git a/Tools/build_comparison_md.py b/Tools/build_comparison_md.py
index bc75d44..ad8e7cd 100644
--- a/Tools/build_comparison_md.py
+++ b/Tools/build_comparison_md.py
@@ -19,10 +19,14 @@ from __future__ import annotations
import os
import re
+import shutil
+import subprocess
DEMO_DIR = "Screenshots/demo"
BLENDER_DIR = "Screenshots/blender"
-OUT = "Screenshots/COMPARISON.md"
+OUT = "Screenshots/README.md"
+BEGIN = ""
+END = ""
# Shots that are about a modelled asset pair with a Blender render; the street and
# building shots have no Blender counterpart because those meshes come from the Geo
@@ -94,9 +98,10 @@ def build():
shots = demo_shots()
renders = blender_renders()
lines = [
- "# Blender vs in-game",
+ "## Blender vs in-game",
"",
- "Generated by `Tools/build_comparison_md.py` — do not edit by hand.",
+ "Regenerated by `Tools/build_comparison_md.py`; everything between the markers is",
+ "rewritten, so edit outside them.",
"",
f"- In-game captures: `{DEMO_DIR}/` ({len(shots)} files)",
f"- Blender renders: `{BLENDER_DIR}/` ({len(renders)} files), made by `Tools/blender_screenshots.py`",
@@ -105,7 +110,7 @@ def build():
"import problem, not a taste problem: scale, material colour, missing parts and split",
"systems all show up here before they show up in a review.",
"",
- "## Side by side",
+ "### Side by side",
"",
"| In-game | Blender | Shot |",
"| --- | --- | --- |",
@@ -128,7 +133,7 @@ def build():
f"{matched} of {len(shots)} in-game captures have a Blender counterpart; the rest are",
"street and building shots whose meshes come from the Geo pipeline rather than a .blend.",
"",
- "## Signoff",
+ "### Signoff",
"",
"One row per in-game screen. `—` in Blender means there is nothing to compare against.",
"",
@@ -148,10 +153,38 @@ def build():
"in [README.md](README.md); this page exists to compare Blender against the engine.",
"",
]
- os.makedirs(os.path.dirname(OUT) or ".", exist_ok=True)
+ block = "\n".join([BEGIN, ""] + lines + ["", END])
+
+ if os.path.exists(OUT):
+ with open(OUT) as handle:
+ existing = handle.read()
+ else:
+ existing = "# Screenshots\n"
+
+ # The comparison lives inside the main review README, between markers, so it is
+ # regenerated in place: the rest of the README is hand-written and must not be
+ # rewritten by a tool.
+ if BEGIN in existing and END in existing:
+ head = existing[:existing.index(BEGIN)]
+ tail = existing[existing.index(END) + len(END):]
+ updated = head + block + tail
+ else:
+ updated = existing.rstrip("\n") + "\n\n" + block + "\n"
+
with open(OUT, "w") as handle:
- handle.write("\n".join(lines))
- print(f"wrote {OUT} ({len(shots)} shots, {matched} paired, {len(renders)} renders)")
+ handle.write(updated)
+ # The README is checked by `prettier --check` in the repo's gates, and a generated
+ # markdown table is never byte-identical to what prettier produces. Format it here so
+ # regenerating the section cannot leave the tree failing a format check.
+ formatter = shutil.which("npx")
+ if formatter:
+ try:
+ subprocess.run([formatter, "--no-install", "prettier", "--write", OUT],
+ check=False, capture_output=True, timeout=120)
+ except (OSError, subprocess.SubprocessError):
+ pass
+
+ print(f"updated {OUT} ({len(shots)} shots, {matched} paired, {len(renders)} renders)")
return 0
diff --git a/Tools/ue_vehicle_materials.py b/Tools/ue_vehicle_materials.py
new file mode 100644
index 0000000..ef0fe1d
--- /dev/null
+++ b/Tools/ue_vehicle_materials.py
@@ -0,0 +1,105 @@
+# 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 replace_material(name, spec):
+ resolved = existing_material_name(name)
+ if resolved is None:
+ log(f"{name}: no imported asset matches - skipped")
+ return None
+ name = resolved
+ 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():
+ if replace_material(name, spec):
+ made += 1
+ 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")
diff --git a/Tools/vehicle_from_template.py b/Tools/vehicle_from_template.py
index 8705368..cf4f7b4 100644
--- a/Tools/vehicle_from_template.py
+++ b/Tools/vehicle_from_template.py
@@ -55,6 +55,102 @@ SYSTEM_RULES = (
)
+def _colour_rank(colour):
+ """Sort key for picking a paint colour: saturation first, then brightness."""
+ return (max(colour) - min(colour), sum(colour))
+
+
+def read_material(material):
+ """Base colour, roughness and metallic for a material, Principled or legacy.
+
+ The template's car paint is a **legacy node graph** - mix shaders over glossy and
+ diffuse BSDFs - not a Principled shader. Reading only Principled skipped it, which is
+ exactly why the car arrived in Unreal near-black: the paint colour never left Blender.
+ For those graphs the colour is taken from the mix nodes, which is where the red lives.
+ """
+ if not material.use_nodes:
+ return None
+ nodes = material.node_tree.nodes
+ bsdf = nodes.get("Principled BSDF")
+ if bsdf:
+ colour = bsdf.inputs["Base Color"].default_value
+ return {
+ "base_color": [round(colour[0], 4), round(colour[1], 4), round(colour[2], 4)],
+ "roughness": round(bsdf.inputs["Roughness"].default_value, 4),
+ "metallic": round(bsdf.inputs["Metallic"].default_value, 4),
+ "source": "principled",
+ }
+ # Legacy graph: the largest colour found on a mix or diffuse node is the paint.
+ best = None
+ roughness = 0.5
+ metallic = 0.0
+ for node in nodes:
+ if node.type in ("MIX", "MIX_RGB"):
+ for socket in node.inputs:
+ try:
+ value = socket.default_value
+ except Exception: # noqa: BLE001 - not every socket has a default
+ continue
+ if not hasattr(value, "__len__") or len(value) < 3:
+ continue
+ candidate = (value[0], value[1], value[2])
+ if max(candidate) < 0.03:
+ continue
+ # Prefer the most saturated colour, not the brightest: the paint's red
+ # (0.49, 0, 0) is darker than the 0.8 grey diffuse beside it, and a
+ # "brightest wins" rule picked the grey - which is how a red car reaches
+ # Unreal as a grey one.
+ if best is None or _colour_rank(candidate) > _colour_rank(best):
+ best = candidate
+ elif node.type == "BSDF_DIFFUSE":
+ colour = node.inputs["Color"].default_value
+ candidate = (colour[0], colour[1], colour[2])
+ if best is None or _colour_rank(candidate) > _colour_rank(best):
+ best = candidate
+ elif node.type == "BSDF_GLOSSY":
+ try:
+ roughness = round(node.inputs["Roughness"].default_value, 4)
+ except Exception: # noqa: BLE001
+ pass
+ if best is None:
+ return None
+ return {
+ "base_color": [round(best[0], 4), round(best[1], 4), round(best[2], 4)],
+ "roughness": roughness,
+ "metallic": metallic,
+ "source": "legacy graph",
+ }
+
+
+def ensure_principled(material):
+ """Rewire a legacy graph to a Principled BSDF so the FBX export carries a colour.
+
+ The template's car paint is a mix-shader graph, and the FBX exporter cannot map that -
+ which is why SM_Vehicle_Body arrived in Unreal holding `WorldGridMaterial` (the default
+ fallback) and rendered black while Blender showed red. Converting in place keeps the
+ colour and makes the export uniform.
+ """
+ if not material.use_nodes:
+ return False
+ nodes = material.node_tree.nodes
+ if nodes.get("Principled BSDF"):
+ return False
+ spec = read_material(material)
+ if not spec:
+ return False
+ nodes.clear()
+ output = nodes.new("ShaderNodeOutputMaterial")
+ bsdf = nodes.new("ShaderNodeBsdfPrincipled")
+ colour = spec["base_color"]
+ bsdf.inputs["Base Color"].default_value = (colour[0], colour[1], colour[2], 1.0)
+ bsdf.inputs["Roughness"].default_value = max(0.05, spec.get("roughness", 0.5))
+ bsdf.inputs["Metallic"].default_value = spec.get("metallic", 0.0)
+ material.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
+ log(f"converted {material.name} to Principled, base "
+ f"{tuple(round(c, 3) for c in colour)}")
+ return True
+
+
def classify(obj):
"""Assign an object to a system by its materials first, then by its name."""
materials = [m.name.lower() for m in obj.data.materials if m]
@@ -234,8 +330,22 @@ def main(argv=None) -> int:
log(f"SM_Vehicle_Body_LOD{index}: {tris} triangles")
bpy.data.objects.remove(lod, do_unlink=True)
+ converted = sum(1 for m in bpy.data.materials if ensure_principled(m))
+ log(f"legacy materials converted for export: {converted}")
+
+ materials = {}
+ for material in bpy.data.materials:
+ found = read_material(material)
+ if found:
+ materials[material.name] = found
+ principled = sum(1 for m in bpy.data.materials
+ if m.use_nodes and m.node_tree.nodes.get("Principled BSDF"))
+ log(f"materials: {len(materials)} read, {principled} of them Principled, "
+ f"{len(materials) - principled} from legacy graphs")
+
report = {
"template": bpy.data.filepath,
+ "materials": materials,
"target_dimensions_m": {"length": TARGET_LENGTH_M, "width": TARGET_WIDTH_M,
"height": TARGET_HEIGHT_M},
"systems": {system: {"object": joined[system].name,