The blockout cleaning pass found the map was rendering with 604 of 620 mesh actors unmaterialed - every kit piece and all theatre trim. Cause: ensure_material() deleted and recreated each /Game/Hollywood/Site/M_* asset on every run, so actors staged by other tools kept references to deleted assets and fell back to the default material. Nothing errored; the map looked merely slightly flat. - Materials are now updated IN PLACE (load, delete_all_material_expressions, rebuild the graph, recompile, save). Material.expressions is protected from Python, so the editing library's deleter is used. - 53 duplicated kit actor labels fixed (both shop piers of a bay shared one name). - New Tools/ue_audit_scene.py gates the scene: every mesh actor has a material, every verified frontage span is within +/-1 m (via ue_placement), no duplicate labels. - render_screenshots.sh now runs the chain in the required order (demo map -> kit -> theatre -> audit -> cameras -> capture) and exits non-zero on an audit failure, because the demo builder rebuilds materials and later staging must follow it. Scene audit: 0 failures. Packaged QA PASS. Spawn-view reference score 0.72.
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit the DemoMap for staged actors that would render wrong.
|
|
|
|
Checks that are cheap and catch the failures this project actually had:
|
|
|
|
1. **No unmaterialed actors.** 604 actors once rendered with the default material
|
|
because the demo builder deleted and recreated the site materials, orphaning
|
|
every reference; the map looked merely "a bit flat".
|
|
2. **Placement spans.** Each verified frontage building is probed against
|
|
`Tools/ue_placement.py`; a building 13 m off its footprint still renders fine,
|
|
which is exactly why it needs a number, not an eye.
|
|
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
|
|
the outliner ambiguous and hid one of them.
|
|
|
|
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
-script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
|
|
|
|
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
|
|
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import os
|
|
import sys
|
|
|
|
import unreal
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import ue_placement # noqa: E402
|
|
|
|
MAP = "/Game/Hollywood/DemoMap"
|
|
RESULT = "/tmp/gigline/scene-audit.txt"
|
|
|
|
lines: list[str] = []
|
|
failures = 0
|
|
|
|
|
|
def log(message: str) -> None:
|
|
lines.append(message)
|
|
print(message)
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
global failures
|
|
failures += 1
|
|
log("FAIL " + message)
|
|
|
|
|
|
def main() -> None:
|
|
if not unreal.EditorLevelLibrary.load_level(MAP):
|
|
log(f"could not load {MAP}")
|
|
return
|
|
|
|
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
|
|
|
# 1. materials
|
|
unmaterialed = []
|
|
for actor in actors:
|
|
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
|
if not comp or not comp.static_mesh:
|
|
continue
|
|
if comp.get_material(0) is None:
|
|
unmaterialed.append(actor.get_actor_label())
|
|
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
|
|
if unmaterialed:
|
|
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
|
|
else:
|
|
log("OK every staged mesh has a material")
|
|
|
|
# 2. placement spans
|
|
for actor in actors:
|
|
label = actor.get_actor_label()
|
|
key = None
|
|
if label.startswith("Frontage_SM_"):
|
|
name = label[len("Frontage_SM_"):]
|
|
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
|
|
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
|
|
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
|
|
elif label == ue_placement.HERO_ACTOR:
|
|
key = "hero"
|
|
if not key:
|
|
continue
|
|
origin, extent = actor.get_actor_bounds(False)
|
|
lo, hi = origin.x - extent.x, origin.x + extent.x
|
|
problem = ue_placement.describe_span_failure(key, lo, hi)
|
|
if problem:
|
|
fail(problem)
|
|
else:
|
|
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
|
|
|
|
# 3. duplicate labels
|
|
counts = collections.Counter(a.get_actor_label() for a in actors)
|
|
duplicates = {k: v for k, v in counts.items() if v > 1}
|
|
if duplicates:
|
|
sample = list(duplicates.items())[:6]
|
|
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
|
|
else:
|
|
log("OK no duplicate actor labels")
|
|
|
|
log(f"== {failures} failure(s) ==")
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as exc: # noqa: BLE001 - surface in the result file
|
|
import traceback
|
|
log(f"FAILED: {type(exc).__name__}: {exc}")
|
|
log(traceback.format_exc())
|
|
|
|
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
|
with open(RESULT, "w", encoding="utf-8") as handle:
|
|
handle.write("\n".join(lines) + "\n")
|