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:
1 parent
bc9fed8e11
commit
8c34eaece0
26 files changed
+171
-41
No files matched your search
@@ -105,6 +105,7 @@ Scripts resolve the repository root as `../..` from their own folder and put the
|
||||
| [`gen/comfyui_generate.py`](gen/comfyui_generate.py) | Drives the **local** ComfyUI at `http://127.0.0.1:8188` (Stability Matrix install). `models` lists checkpoints; `txt2img` renders through the HTTP API; **`texture` renders material sheets with the project's documented texture workflows** (see below). Every run writes a provenance sidecar (model, prompt, seed, settings, full graph). Two candidates by default — same prompt and seed, both scored by high-frequency detail, the winner takes `--out`, the loser is kept beside it. Local and human-triggered; no credential. |
|
||||
| [`gen/generate_facade_textures.py`](gen/generate_facade_textures.py) | Procedural, deterministic facade texture maps (ashlar base colour + roughness, stucco, asphalt, concrete) written with a pure-zlib PNG encoder — no model, no network, byte-identical on every run. |
|
||||
| [`gen/generate_character_textures.py`](gen/generate_character_textures.py) | Procedural character texture maps, same deterministic approach as the facade set. |
|
||||
| [`gen/generate_banner_texture.py`](gen/generate_banner_texture.py) | Draws the hero-facade banner (`JIMMY KIMMEL LIVE` in gold on crimson) with Pillow + a system font — exact text, deterministic, no model. An image model would mangle a trademark string, and Qwen-Image 2.1 is research-only, so this is the lettering path. `--out Art/textures/generated/T_Banner_KIMMEL.png` by default. |
|
||||
| [`gen/facade_detail_concepts.py`](gen/facade_detail_concepts.py) | Generates **frontal building plates** (whole elevation, entrance in its facade, or building with neighbours) from a local reference photograph with an image model (OpenRouter); close-up detail crops are deliberately not requested because they cannot be measured for proportion. `--list` is free and shows which studies have their reference present; `--all`/`--only <name>` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. |
|
||||
| [`review/judge_screenshot.py`](review/judge_screenshot.py) | (see Visual review) |
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -47,6 +47,8 @@ def log(msg):
|
||||
# reference facade is coursed rubble-ashlar, not a flat tint, and a texture is what carries that at 45 m.
|
||||
FACADE_ALBEDO = "Art/textures/generated/T_FacadeAshlar_AI.png"
|
||||
FACADE_ALBEDO_ASSET = "T_FacadeAshlar_AI"
|
||||
BANNER_ALBEDO = "Art/textures/generated/T_Banner_KIMMEL.png"
|
||||
BANNER_ALBEDO_ASSET = "T_Banner_KIMMEL"
|
||||
TEXTURE_DIR = "/Game/Hollywood/Textures"
|
||||
|
||||
FACADE_TINTS = {
|
||||
@@ -67,26 +69,26 @@ FACADE_TINTS = {
|
||||
}
|
||||
|
||||
|
||||
def import_albedo():
|
||||
"""Import the generated ashlar albedo, if it is on disk. Returns the asset or None."""
|
||||
def import_texture(rel_path, asset_name):
|
||||
"""Import a generated texture, if it is on disk. Returns the asset or None."""
|
||||
# unreal.Paths.project_dir() is Game/GigLine, not the repository root: the textures live beside Art/,
|
||||
# two levels above this file.
|
||||
# Tools/unreal/<this file> -> repository root is three levels up.
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
path = os.path.join(repo_root, FACADE_ALBEDO)
|
||||
path = os.path.join(repo_root, rel_path)
|
||||
if not os.path.exists(path):
|
||||
log(f"albedo missing: {path}")
|
||||
log(f"texture missing: {path}")
|
||||
return None
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = path
|
||||
task.destination_path = TEXTURE_DIR
|
||||
task.destination_name = FACADE_ALBEDO_ASSET
|
||||
task.destination_name = asset_name
|
||||
task.automated = True
|
||||
task.save = True
|
||||
task.replace_existing = True
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
asset = unreal.EditorAssetLibrary.load_asset(f"{TEXTURE_DIR}/{FACADE_ALBEDO_ASSET}")
|
||||
log(f"albedo imported: {asset.get_path_name() if asset else 'FAILED'}")
|
||||
asset = unreal.EditorAssetLibrary.load_asset(f"{TEXTURE_DIR}/{asset_name}")
|
||||
log(f"texture imported: {asset.get_path_name() if asset else 'FAILED'}")
|
||||
return asset
|
||||
|
||||
|
||||
@@ -102,7 +104,8 @@ def tint_imported_materials():
|
||||
and the owner's review was that the elevation read near-white grey.
|
||||
"""
|
||||
library = unreal.MaterialEditingLibrary
|
||||
albedo = import_albedo()
|
||||
albedo = import_texture(FACADE_ALBEDO, FACADE_ALBEDO_ASSET)
|
||||
banner_tex = import_texture(BANNER_ALBEDO, BANNER_ALBEDO_ASSET)
|
||||
built = 0
|
||||
for base, rgb in FACADE_TINTS.items():
|
||||
path = f"{DEST}/{base}_Ref"
|
||||
@@ -114,6 +117,7 @@ def tint_imported_materials():
|
||||
material, unreal.MaterialExpressionConstant3Vector, -600, 200)
|
||||
colour.set_editor_property("constant", unreal.LinearColor(rgb[0], rgb[1], rgb[2], 1.0))
|
||||
source = colour
|
||||
kind = "tint"
|
||||
if albedo is not None and base in ("M_Facade_Stone", "M_Facade_StoneShadow"):
|
||||
sample = library.create_material_expression(
|
||||
material, unreal.MaterialExpressionTextureSampleParameter2D, -900, -100)
|
||||
@@ -130,11 +134,25 @@ def tint_imported_materials():
|
||||
library.connect_material_expressions(sample, "RGB", multiply, "A")
|
||||
library.connect_material_expressions(colour, "", multiply, "B")
|
||||
source = multiply
|
||||
kind = "albedo x tint"
|
||||
elif banner_tex is not None and base == "M_Facade_Banner":
|
||||
# The banner texture carries both the red ground and the gold lettering, so it
|
||||
# replaces the tint outright (tiling 1.0, not the 6x stone coursing).
|
||||
sample = library.create_material_expression(
|
||||
material, unreal.MaterialExpressionTextureSampleParameter2D, -900, -100)
|
||||
sample.set_editor_property("parameter_name", f"Banner_{base}")
|
||||
sample.set_editor_property("texture", banner_tex)
|
||||
sample.set_editor_property("sampler_type", unreal.MaterialSamplerType.SAMPLERTYPE_COLOR)
|
||||
uvs = library.create_material_expression(
|
||||
material, unreal.MaterialExpressionTextureCoordinate, -1150, -100)
|
||||
library.connect_material_expressions(uvs, "", sample, "UVs")
|
||||
source = sample
|
||||
kind = "banner"
|
||||
library.connect_material_property(source, "", unreal.MaterialProperty.MP_BASE_COLOR)
|
||||
library.recompile_material(material)
|
||||
saved = unreal.EditorAssetLibrary.save_loaded_asset(material)
|
||||
built += 1
|
||||
log(f"{base}_Ref: {'albedo x tint' if source is not colour else 'tint'} {rgb}, saved {saved}")
|
||||
log(f"{base}_Ref: {kind} {rgb}, saved {saved}")
|
||||
log(f"authored {built} facade materials")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user