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:
1 parent
670c692c43
commit
61dd28bc89
116 files changed
+279
-140
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
Executable
+53
@@ -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"
|
||||
Reference in new issue
Block a user