feat(hero): banner lettering, re-render the three staging shots; defer the rest to 0.9.0

Banner lettering (JIMMY KIMMEL LIVE): new Tools/gen/generate_banner_texture.py
draws the exact title with Pillow + Liberation Sans Bold - an image model would
mangle a trademark string, and Qwen-Image 2.1 is research-only. Gold JIMMY /
KIMMEL / LIVE stacked on a crimson ground. ue_hero_facade_import.py generalises
import_albedo to import_texture and wires the banner into M_Facade_Banner_Ref as
Base Color (tiling 1.0), replacing the flat dark tint with a brighter red per the
owner's 'less dark, more bright'.

Re-rendered all three ReviewMap staging shots (front, oblique, portals) and
updated their Screenshots/README.md status rows.

The hero facade rework is now closed at 'surface detail that reads at 45 m'. The
remaining elevation work - volute capital depth, lattice grille depth, and the
full realism pass - is deferred to 0.9.0 per the owner, and the 0.9.0 hero item
records the hand-off.
This commit is contained in:
zwitschi committed 2026-11-10 15:27:08 +01:00
1 parent bc9fed8e11
commit 8c34eaece0
26 files changed
+171 -41

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Generate the JIMMY KIMMEL LIVE banner texture for the hero facade.
The three event banners are flat panels; the lettering is text and cannot be
read as geometry at 45 m. An image model would mangle a specific trademark
string, so the text is drawn here with Pillow and a system font instead:
deterministic, exact, and (unlike Qwen-Image 2.1) not research-only.
The banner face is 1.35 m wide x 6.2 m tall (aspect 1:4.59). The texture is
written at 512 x 2304 (aspect 1:4.5), a 2% stretch that is invisible at the
review distance.
python3 Tools/gen/generate_banner_texture.py
python3 Tools/gen/generate_banner_texture.py --out Art/textures/generated/T_Banner_KIMMEL.png
Requires Pillow (host-only tool, not a gate; standard library is not enough to
rasterise a font). The text is a trademark reproduced for internal development
review only - record it in THIRD_PARTY_NOTICES.md before any public build.
"""
from __future__ import annotations
import argparse
import os
from PIL import Image, ImageDraw, ImageFont
# Banner face aspect: 1.35 m wide x 6.2 m tall. 512 x 2304 keeps that to within 2 %.
WIDTH = 512
HEIGHT = 2304
# sRGB colours. The banner is a deep event red, the lettering is gold; both match the
# photographs (and the owner's "less dark, more bright" direction for the elevation).
BANNER = (186, 24, 24)
GOLD = (232, 198, 90)
FONT_BOLD = "/usr/share/fonts/liberation-sans-fonts/LiberationSans-Bold.ttf"
FONT_REGULAR = "/usr/share/fonts/liberation-sans-fonts/LiberationSans-Regular.ttf"
# Three stacked words, as the real vertical banners arrange the title. `size` is the
# font height in texture pixels; the block is centred vertically in the banner.
LINES = [
("JIMMY", 150, FONT_BOLD),
("KIMMEL", 150, FONT_BOLD),
("LIVE", 120, FONT_REGULAR),
]
LINE_GAP = 70 # pixels between baselines beyond the font ascent
def _font(path: str, size: int) -> ImageFont.FreeTypeFont:
if not os.path.exists(path):
raise SystemExit(f"font missing: {path}")
return ImageFont.truetype(path, size)
def draw_banner(out: str) -> None:
img = Image.new("RGB", (WIDTH, HEIGHT), BANNER)
draw = ImageDraw.Draw(img)
fonts = [(text, size, _font(path, size)) for text, size, path in LINES]
# Measure the whole block so it sits centred vertically in the 6.2 m banner.
ascents = [f.getbbox(text)[3] for text, _, f in fonts]
block_h = sum(ascents) + LINE_GAP * (len(fonts) - 1)
y = (HEIGHT - block_h) // 2
for text, size, font in fonts:
bbox = font.getbbox(text)
text_w = bbox[2] - bbox[0]
x = (WIDTH - text_w) // 2 - bbox[0]
draw.text((x, y), text, font=font, fill=GOLD)
y += bbox[3] + LINE_GAP
os.makedirs(os.path.dirname(os.path.abspath(out)) or ".", exist_ok=True)
img.save(out)
print(f"wrote {out} ({WIDTH}x{HEIGHT})")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--out", default="Art/textures/generated/T_Banner_KIMMEL.png")
args = parser.parse_args()
draw_banner(args.out)
if __name__ == "__main__":
main()