feat(hero): one-command iteration loop, first judged pass, comparison plates

Tools/hero_iterate.sh runs the whole chain - chmod the Blender files, build, import, render, judge, plate -
and fails loudly at each step with its log under /tmp/gigline. Tools/hero_compare.py builds the side-by-side
plate (reference | rendered, one height, labelled); two are committed for review.

This pass: the stone is warmer in Blender (0.66/0.60/0.48 against the near-white blockout) and the banners are
1.35 x 6.2 m hung from 7.2 m, matching the reference proportion instead of the 1.55 x 5.4 that sat too low and
too fat.

Iteration 1 judge verdict, confidence 0.9: lacks warm sandstone, lacks fluted column detail, banners wrong,
no doorways, no inscription or medallions. Checked against the plate, two of those are wrong (120 flute
strips exist and cast shadows; three 1.2 m deep doorways with steps exist and read black) and three are real.
The split is recorded in TODO.md with the next action for each - the important one being that the stone colour
must change in the UE material (ue_hero_facade_import.py), not in the Blender material, which the render does
not use.
This commit is contained in:
zwitschi committed 2026-11-10 08:28:06 +01:00
1 parent 670c692c43
commit 61dd28bc89
116 files changed
+279 -140

No files matched your search

+2
View File
@@ -55,6 +55,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| `ue_import_site.py` | Imports the six site FBXs into `/Game/Hollywood/Site`, setting collision complexity per mesh |
| `ue_hero_facade_import.py` | Imports the façade and places it on the measured building line |
| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start |
| `hero_iterate.sh` | Full hero-facade loop in one command: chmod the Blender files, build the facade, import it, render the capture set, judge the shot and write a comparison plate. `bash Tools/hero_iterate.sh [shot]`. |
| `hero_compare.py` | Side-by-side plate of a reference photograph and a rendered capture, scaled to one height and labelled. `--reference`, `--render`, `--out`. |
| `ue_clear_simple_collision.py` | Removes the FBX import's auto-generated convex hulls from the site and street meshes and sets them to complex-as-simple, so physics bodies are not pushed out of a 279 m hull. |
| `ue_vehicle_probe_map.py` | Copies `ReviewMap` to `VehicleProbe`, optionally adds one demo mesh and the Chaos pawn, and saves it, to bisect which map content throws a physics body into the air. Set `GIGLINE_PROBE_ASSET` to stage one mesh; unset for the control. |
| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views |
+56
View File
@@ -0,0 +1,56 @@
"""Build a side-by-side plate from a reference photograph and a rendered capture.
python3 Tools/hero_compare.py \
--reference Art/reference/hero-masonic/02-masonictemple01.jpg \
--render Screenshots/review/review-hero-facade-front.png \
--out Screenshots/review/hero-compare-front.png
Both halves are scaled to the same height and labelled, so the two can be read at a glance instead of
flipping between files. Reference photography is git-ignored and never committed; only the plate is.
Result: the plate path on stdout.
"""
import argparse
import os
from PIL import Image, ImageDraw
LABEL_H = 56
TARGET_H = 900
def load_scaled(path, height):
image = Image.open(path).convert("RGB")
width = int(image.width * height / image.height)
return image.resize((width, height), Image.LANCZOS)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--reference", required=True)
parser.add_argument("--render", required=True)
parser.add_argument("--out", required=True)
parser.add_argument("--left-label", default="reference")
parser.add_argument("--right-label", default="rendered")
args = parser.parse_args()
left = load_scaled(args.reference, TARGET_H)
right = load_scaled(args.render, TARGET_H)
gap = 16
width = left.width + right.width + gap * 3
plate = Image.new("RGB", (width, TARGET_H + LABEL_H + gap * 2), (24, 24, 26))
plate.paste(left, (gap, LABEL_H + gap))
plate.paste(right, (gap * 2 + left.width, LABEL_H + gap))
draw = ImageDraw.Draw(plate)
draw.text((gap, 18), args.left_label, fill=(235, 235, 235))
draw.text((gap * 2 + left.width, 18), args.right_label, fill=(235, 235, 235))
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
plate.save(args.out)
print(args.out)
main()
+8 -4
View File
@@ -129,8 +129,10 @@ def build():
for obj in list(bpy.data.objects):
bpy.data.objects.remove(obj, do_unlink=True)
stone = make_material("M_Facade_Stone", (0.78, 0.74, 0.66, 1.0), 0.85)
stone_dark = make_material("M_Facade_StoneShadow", (0.62, 0.59, 0.53, 1.0), 0.85)
# Warm limestone, sampled from 02-masonictemple01.jpg: the reference is a yellow-grey sandstone,
# nowhere near the near-white the blockout used.
stone = make_material("M_Facade_Stone", (0.66, 0.60, 0.48, 1.0), 0.82)
stone_dark = make_material("M_Facade_StoneShadow", (0.52, 0.47, 0.38, 1.0), 0.85)
glass = make_material("M_Facade_GlassDark", (0.05, 0.06, 0.07, 1.0), 0.25)
banner = make_material("M_Facade_Banner", (0.66, 0.06, 0.05, 1.0), 0.9)
@@ -340,8 +342,10 @@ def build():
# --- banners -----------------------------------------------------------
# Three vertical event banners hung from the column line, as the recordings show.
for i, x in enumerate(portal_xs, start=1):
cube(f"Hero_Banner_{i}", x, -1.05, 7.9, 1.55, 0.06, 5.4, banner)
note("Hero_Banner_x3", (1.55, 0.06, 5.4), "photographs: JIMMY KIMMEL LIVE banners on the column line")
# The reference banners hang from just below the capitals down to the portal heads:
# 6.2 m tall and 1.35 m wide, not the 5.4 x 1.55 blockout that sat too low and too fat.
cube(f"Hero_Banner_{i}", x, -1.05, 7.2, 1.35, 0.06, 6.2, banner)
note("Hero_Banner_x3", (1.35, 0.06, 6.2), "photographs: JIMMY KIMMEL LIVE banners on the column line")
# --- windows -----------------------------------------------------------
# Two ranks of windows between the columns and behind the colonnade.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Full hero-facade iteration: build -> import -> render -> judge -> comparison plate.
#
# bash Tools/hero_iterate.sh [shot]
#
# `shot` defaults to review-hero-facade-front. Every step writes to /tmp/gigline/ so a failure can be
# read back: Unreal commandlets swallow print().
#
# Why a script and not four remembered commands: the loop is run once per modelling change, and the
# order is load-bearing. The Blender save happens BEFORE the FBX export, so a read-only .blend makes
# the build look successful while the old FBX is reused silently - hence the chmod at the top.
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1
ENGINE="${ENGINE:-/run/media/system/DATA/Engines/UE_5.8.4}"
BLENDER_FILES="${BLENDER_FILES:-/var/lib/flatpak/app/org.blender.Blender/x86_64/stable/active/files}"
UPROJECT="$(pwd)/Game/GigLine/GigLine.uproject"
SHOT="${1:-review-hero-facade-front}"
REFERENCE="${REFERENCE:-Art/reference/hero-masonic/02-masonictemple01.jpg}"
step() { printf '\n== %s\n' "$1"; }
step "1/5 blender files writable"
find Art -name '*.blend*' -exec chmod u+w {} \;
step "2/5 build the facade"
LD_LIBRARY_PATH="$BLENDER_FILES/blender/lib" "$BLENDER_FILES/blender/blender" -b --factory-startup \
--python Tools/hero_facade_build.py -- \
--out /tmp/gigline/hero-facade.fbx \
--save-blend Art/blender/hero/hero-facade-2026-11-07.blend \
--report Art/blender/hero/hero-facade-2026-11-07.json \
> /tmp/gigline/hero-build.log 2>&1 || { echo "build failed - see /tmp/gigline/hero-build.log"; exit 1; }
grep -E 'exported FBX' /tmp/gigline/hero-build.log || { echo "no FBX written"; exit 1; }
step "3/5 import into Unreal"
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$UPROJECT" -run=pythonscript \
-script="$(pwd)/Tools/ue_hero_facade_import.py" -nullrhi -unattended -nopause -stdout \
> /tmp/gigline/hero-import.log 2>&1
grep -q 'save_map' /tmp/gigline/hero-facade-import.txt || { echo "import failed - see /tmp/gigline/hero-facade-import.txt"; exit 1; }
step "4/5 render the capture set"
bash Tools/render_screenshots.sh > /tmp/gigline/hero-render.log 2>&1 || { echo "render failed - see /tmp/gigline/hero-render.log"; exit 1; }
step "5/5 judge and compare"
python3 Tools/judge_screenshot.py --image "Screenshots/review/${SHOT}.png" \
--expect "A neoclassical Masonic temple facade: warm yellow-grey sandstone with horizontal ashlar courses, six giant fluted Ionic columns on square pedestals, three tall red vertical banners reading JIMMY KIMMEL LIVE hanging between the columns, dark lattice metal grilles in the window bays behind the colonnade, three recessed dark entrance doorways with steps, above the columns a carved inscription band flanked by two round medallions, topped by a crested parapet." \
| tee /tmp/gigline/hero-judge.txt | tail -2
python3 Tools/hero_compare.py --reference "$REFERENCE" \
--render "Screenshots/review/${SHOT}.png" \
--out "Screenshots/review/hero-compare-${SHOT#review-hero-}.png"
echo
echo "plate: Screenshots/review/hero-compare-${SHOT#review-hero-}.png"