Use the photo-anchored ashlar limestone on the masonic facade
Full build / Import + headless tests (push) Successful in 49s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 15:40:25 +01:00
1 parent 05cb6a43cb
commit f95a685934
24 files changed
+111 -10

No files matched your search

+5 -1
View File
@@ -393,10 +393,14 @@ facade evidence is still the 2009 CC BY frame. The 8 downloaded candidates are i
- [ ] Run the assisted Blender pipeline end to end on the first block (OSM import → `inspect` → - [ ] Run the assisted Blender pipeline end to end on the first block (OSM import → `inspect` →
retopo/UV assist → atlas → `audit`), and document the exact commands in retopo/UV assist → atlas → `audit`), and document the exact commands in
`docs/asset-tooling.md`. `docs/asset-tooling.md`.
- [ ] Building-appearance realism pass, measured against the reference set: bring the masonic - [-] Building-appearance realism pass, measured against the reference set: bring the masonic
facade's material response (limestone value/roughness, joint lines, weathering streaks), facade's material response (limestone value/roughness, joint lines, weathering streaks),
the banner/canopy and marquee materials, and the night emissive balance to the level of the the banner/canopy and marquee materials, and the night emissive balance to the level of the
2009 facade photo and the 2026 street frames; capture before/after and record each delta. 2009 facade photo and the 2026 street frames; capture before/after and record each delta.
(Ashlar field landed: `limestone_ashlar` tile generated reference-anchored from the facade
photo, 768 px over ~4.6 m of wall = ~167 px/m, promoted via the picks pipeline; facade
field + tint warm-shift live behind `VENUE_SPECS`, recapture committed and the day board
rebuilt. Still open: night emissive balance and banner/canopy materials.)
- [ ] Upgrade the facade from flat material bands to photoreal detail: either texture-projected - [ ] Upgrade the facade from flat material bands to photoreal detail: either texture-projected
detail (Path B below) or AI-generated material tiles derived from the facade photo (with detail (Path B below) or AI-generated material tiles derived from the facade photo (with
provenance), then verify texel density, tiling and the night look. provenance), then verify texel density, tiling and the night look.
Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

+4
View File
@@ -73,6 +73,10 @@ not texture or geometry sources.
- Local lights use linear attenuation (`omni_attenuation = 1.0`). Godot treats - Local lights use linear attenuation (`omni_attenuation = 1.0`). Godot treats
`0.0` as constant falloff, which previously turned the night facade into a `0.0` as constant falloff, which previously turned the night facade into a
flat yellow wall; the sconces now pool light instead. flat yellow wall; the sconces now pool light instead.
- The masonic facade field uses the `limestone_ashlar` pick: a seamless ashlar
tile generated reference-anchored on the 2009 facade photo (CC BY 2.0),
768 px over ~4.6 m of wall (~167 px/m), so the wall carries course joints and
per-block value variation instead of flat plaster.
## References ## References
+1 -1
View File
@@ -13,6 +13,6 @@ attribution lives in the caption and the [refs sidecars](../refs/README.md)).
| Board | Caption | | Board | Caption |
| --- | --- | | --- | --- |
| ![three-up board: elcapitan day](elcapitan-day-board.jpg) | El Capitan facade, day. Photo: [CC BY 2.0, woo, 2009](https://commons.wikimedia.org/wiki/File:El_Capitan_Theater_frontal_fachade-The_Masonic_Temple.jpg) via `refs/hollywood`. Concept: photo-anchored generation. Capture: current build. | | ![three-up board: elcapitan day](elcapitan-day-board.jpg) | El Capitan facade, day. Photo: [CC BY 2.0, woo, 2009](https://commons.wikimedia.org/wiki/File:El_Capitan_Theater_frontal_fachade-The_Masonic_Temple.jpg) via `refs/hollywood`. Concept: photo-anchored generation. Capture: current build with the ashlar limestone field (`limestone_ashlar`, anchored on the same photo). |
| ![three-up board: elcapitan night](elcapitan-night-board.jpg) | El Capitan block, night. Photo: CC BY-SA 3.0 (reviewed; redistribution only, no derived assets) via `refs/hollywood`. Concept: photo-anchored night restage. Capture: current build. | | ![three-up board: elcapitan night](elcapitan-night-board.jpg) | El Capitan block, night. Photo: CC BY-SA 3.0 (reviewed; redistribution only, no derived assets) via `refs/hollywood`. Concept: photo-anchored night restage. Capture: current build. |
| ![three-up board: jokeshop day](jokeshop-day-board.jpg) | Joke Shop frontage, day. Photo: public domain (2006) via `refs/joke-shop`. Concept: generated. Capture: current build. | | ![three-up board: jokeshop day](jokeshop-day-board.jpg) | Joke Shop frontage, day. Photo: public domain (2006) via `refs/joke-shop`. Concept: generated. Capture: current build. |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"board": "elcapitan-day-board.jpg", "board": "elcapitan-day-board.jpg",
"caption": "El Capitan facade, day: 2009 CC BY photo | photo-anchored concept | current capture", "caption": "El Capitan facade, day. Photo: CC BY 2.0 2009 via refs/hollywood. Concept: photo-anchored. Capture: ashlar limestone pass (limestone_ashlar, 768 px tile, ~167 px/m).",
"photo": "docs/screens/refs/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg", "photo": "docs/screens/refs/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"concept": "docs/screens/concepts/hollywood_boulevard/hollywood_boulevard_entrance_day-v2.png", "concept": "docs/screens/concepts/hollywood_boulevard/hollywood_boulevard_entrance_day-v2.png",
"capture": "docs/screenshots/01-venue-overview.png" "capture": "docs/screenshots/01-venue-overview.png"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 841 KiB

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+11
View File
@@ -28,6 +28,14 @@ const T_CONCRETE_NORMAL := preload(PICK_DIR + "sidewalk_pavers_normal.png")
const T_ASPHALT_NORMAL := preload(PICK_DIR + "asphalt_normal.png") const T_ASPHALT_NORMAL := preload(PICK_DIR + "asphalt_normal.png")
const T_ASPHALT_WET_NORMAL := preload(PICK_DIR + "asphalt_wet_normal.png") const T_ASPHALT_WET_NORMAL := preload(PICK_DIR + "asphalt_wet_normal.png")
const T_LIMESTONE_NORMAL := preload(PICK_DIR + "stone_steps_normal.png") const T_LIMESTONE_NORMAL := preload(PICK_DIR + "stone_steps_normal.png")
## Ashlar limestone for the masonic temple field wall: large dressed blocks with
## thin course joints, generated against the licensed facade photo (the
## `stone_steps` tile reads as small pavers, not wall ashlar).
## Ashlar limestone for the masonic temple field wall: large dressed blocks with
## thin course joints, generated against the licensed facade photo (the
## `stone_steps` tile reads as small pavers, not wall ashlar).
const T_ASHLAR := preload(PICK_DIR + "limestone_ashlar.png")
const T_ASHLAR_NORMAL := preload(PICK_DIR + "limestone_ashlar_normal.png")
## Roughness / metallic pairs per surface role. Exposed so tests and tooling can ## Roughness / metallic pairs per surface role. Exposed so tests and tooling can
## assert the profiles instead of hard-coding numbers twice. ## assert the profiles instead of hard-coding numbers twice.
@@ -107,6 +115,9 @@ static func concrete(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(9
static func limestone(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(6, 3)) -> StandardMaterial3D: static func limestone(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(6, 3)) -> StandardMaterial3D:
return textured(T_LIMESTONE, tint, uv_scale, "stone", T_LIMESTONE_NORMAL) return textured(T_LIMESTONE, tint, uv_scale, "stone", T_LIMESTONE_NORMAL)
static func ashlar(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(6, 3)) -> StandardMaterial3D:
return textured(T_ASHLAR, tint, uv_scale, "stone", T_ASHLAR_NORMAL)
static func asphalt(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(30, 7), static func asphalt(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(30, 7),
wet: bool = false) -> StandardMaterial3D: wet: bool = false) -> StandardMaterial3D:
var texture: Texture2D = T_ASPHALT_WET if wet else T_ASPHALT var texture: Texture2D = T_ASPHALT_WET if wet else T_ASPHALT
+8 -3
View File
@@ -9,6 +9,7 @@ const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
const T_ASPHALT := preload(PICK_DIR + "asphalt.png") const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
const T_AWNING := preload(PICK_DIR + "awning_canvas.png") const T_AWNING := preload(PICK_DIR + "awning_canvas.png")
const T_LIMESTONE := preload(PICK_DIR + "stone_steps.png") const T_LIMESTONE := preload(PICK_DIR + "stone_steps.png")
const T_ASHLAR := preload(PICK_DIR + "limestone_ashlar.png")
## One skyline panorama per shift phase (realism pass): hazy daylight, warm dusk ## One skyline panorama per shift phase (realism pass): hazy daylight, warm dusk
## with lit windows, and a night city. The old single strip was a black silhouette. ## with lit windows, and a night city. The old single strip was a black silhouette.
const T_SKYLINE_DAY := preload(PICK_DIR + "skyline_day.png") const T_SKYLINE_DAY := preload(PICK_DIR + "skyline_day.png")
@@ -86,8 +87,10 @@ const VENUE_SPECS := {
## a recessed central entrance and two street-level side doors. ## a recessed central entrance and two street-level side doors.
"architecture": "masonic", "architecture": "masonic",
"facade_width": 29.6, "facade_height": 14.5, "facade_depth": 8.0, "facade_width": 29.6, "facade_height": 14.5, "facade_depth": 8.0,
"facade_center_z": -8.0, "facade_texture": T_LIMESTONE, "facade_center_z": -8.0, "facade_texture": T_ASHLAR,
"facade_tint": Color(0.88, 0.86, 0.82), "facade_role": "stone", # Photo target: warm cream limestone, mid value ~0.78 (the old tint was
# a flat cool 0.88 that read as plaster in the day capture).
"facade_tint": Color(0.85, 0.81, 0.74), "facade_role": "stone",
"window_columns": [], "window_rows": [], "window_columns": [], "window_rows": [],
"stair_width": 12.0, "stair_count": 8, "stair_tread": 0.38, "stair_height": 0.10, "stair_width": 12.0, "stair_count": 8, "stair_tread": 0.38, "stair_height": 0.10,
"landing_depth": 4.0, "landing_center_z": -1.8, "landing_depth": 4.0, "landing_center_z": -1.8,
@@ -929,8 +932,10 @@ func _build_masonic_building() -> void:
# Main stone mass. The real building is brick/concrete clad in a light # Main stone mass. The real building is brick/concrete clad in a light
# limestone-like render; granite/limestone keeps the block consistent with # limestone-like render; granite/limestone keeps the block consistent with
# the existing PBR surface library. # the existing PBR surface library.
# Ashlar courses are ~0.55 m tall: the 768 px tile covers ~4.4 m of wall
# height, so the facade repeats it 3.2× vertically and 6.4× across.
var facade := _add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), var facade := _add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz),
stone, false, T_LIMESTONE, Vector2(w * 0.22, h * 0.18), "stone") stone, false, T_ASHLAR, Vector2(w * 0.216, h * 0.22), "stone")
facade.name = "MasonicFacade" facade.name = "MasonicFacade"
# Rusticated base and string courses. # Rusticated base and string courses.
_add_box(building, Vector3(w + 0.3, 1.0, d + 0.3), Vector3(0, 0.5, cz + 0.12), _add_box(building, Vector3(w + 0.3, 1.0, d + 0.3), Vector3(0, 0.5, cz + 0.12),
+28 -4
View File
@@ -539,9 +539,21 @@ def _fit(img: Image.Image, max_dim: int = MAX_DIM) -> Image.Image:
def generate_ai_textures(prompt: str, name: str, model: str, size: int, def generate_ai_textures(prompt: str, name: str, model: str, size: int,
transparent: bool = False) -> None: transparent: bool = False,
"""Generate one texture via OpenRouter and save it under assets/textures/.""" reference_path=None) -> None:
img = generate_image(prompt, model, size, transparent) """Generate one texture via OpenRouter and save it under assets/textures/.
With `reference_path`, the request is image-to-image (licence-cleared
reference photo anchoring value and detail); the returned frame is then
centre-cropped to the requested square so tile wraps stay predictable.
"""
img = generate_image(prompt, model, size, transparent,
reference_path=reference_path)
if reference_path is not None:
side = min(img.size)
left = (img.width - side) // 2
top = (img.height - side) // 2
img = img.crop((left, top, left + side, top + side))
img = _fit(img) img = _fit(img)
if transparent: if transparent:
img = img.convert("RGBA") img = img.convert("RGBA")
@@ -556,7 +568,11 @@ def _run_ai(args: argparse.Namespace) -> None:
if args.prompt: if args.prompt:
if not args.name: if not args.name:
sys.exit("error: --name is required when --prompt is given") sys.exit("error: --name is required when --prompt is given")
generate_ai_textures(args.prompt, args.name, args.model, args.size) if bool(args.reference) != bool(args.reference_licence):
sys.exit("error: --reference and --reference-licence must be given "
"together (the licence note records the sidecar clearance)")
generate_ai_textures(args.prompt, args.name, args.model, args.size,
reference_path=args.reference or None)
return return
for name, prompt, transparent in AI_TEXTURES: for name, prompt, transparent in AI_TEXTURES:
print(f"generating {name} ...") print(f"generating {name} ...")
@@ -579,6 +595,14 @@ def _build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument( parser.add_argument(
"--model", default=DEFAULT_IMAGE_MODEL, "--model", default=DEFAULT_IMAGE_MODEL,
help=f"image model slug (default: {DEFAULT_IMAGE_MODEL})") help=f"image model slug (default: {DEFAULT_IMAGE_MODEL})")
parser.add_argument(
"--reference", default="",
help="licence-cleared photo (listed in a refs-board SIDECAR.md) used "
"as an image-to-image anchor; requires --reference-licence")
parser.add_argument(
"--reference-licence", default="",
help="licence label from the photo's sidecar row, recorded in the "
"generation provenance")
parser.add_argument( parser.add_argument(
"--size", type=int, default=GEN_SIZE, "--size", type=int, default=GEN_SIZE,
help=f"square size requested from the API in px (min {GEN_SIZE}); " help=f"square size requested from the API in px (min {GEN_SIZE}); "
+1
View File
@@ -230,6 +230,7 @@ PICKS = {
"skyline_night": "skyline_night-v4", "skyline_night": "skyline_night-v4",
"brick_facade": "brick_facade", "brick_facade": "brick_facade",
"stone_steps": "stone_steps-v2", "stone_steps": "stone_steps-v2",
"limestone_ashlar": "limestone_ashlar",
"sidewalk_pavers": "sidewalk_pavers", "sidewalk_pavers": "sidewalk_pavers",
"asphalt": "asphalt", "asphalt": "asphalt",
"asphalt_wet": "asphalt_wet", "asphalt_wet": "asphalt_wet",
+52
View File
@@ -0,0 +1,52 @@
"""Tests for the reference-anchored texture path (no network)."""
import unittest
from unittest import mock
from PIL import Image
import generate_textures as gt
class ReferenceGateTests(unittest.TestCase):
def test_reference_without_licence_note_is_refused(self):
for argv in (["--ai", "--prompt", "x", "--name", "a.png",
"--reference", "r.jpg"],
["--ai", "--prompt", "x", "--name", "a.png",
"--reference-licence", "CC BY 2.0"]):
with mock.patch.object(gt, "generate_ai_textures") as gen:
with self.assertRaises(SystemExit):
gt._run_ai(gt._build_arg_parser().parse_args(argv))
gen.assert_not_called()
def test_reference_pair_calls_with_the_path(self):
argv = ["--ai", "--prompt", "x", "--name", "a.png",
"--reference", "r.jpg", "--reference-licence", "CC BY 2.0"]
with mock.patch.object(gt, "generate_ai_textures") as gen:
gt._run_ai(gt._build_arg_parser().parse_args(argv))
gen.assert_called_once_with("x", "a.png", gt.DEFAULT_IMAGE_MODEL,
gt.GEN_SIZE, reference_path="r.jpg")
class ReferenceCropTests(unittest.TestCase):
def test_reference_results_are_cropped_square_before_save(self):
wide = Image.new("RGB", (1344, 768), (10, 20, 30))
with mock.patch.object(gt, "generate_image", return_value=wide) as gen, \
mock.patch.object(gt, "save") as saved:
gt.generate_ai_textures("p", "t.png", "m", 1024,
reference_path="r.jpg")
gen.assert_called_once()
img = saved.call_args[0][0]
self.assertEqual(img.size, (768, 768))
def test_plain_results_keep_their_size(self):
wide = Image.new("RGB", (1344, 768), (10, 20, 30))
with mock.patch.object(gt, "generate_image", return_value=wide), \
mock.patch.object(gt, "save") as saved:
gt.generate_ai_textures("p", "t.png", "m", 1024)
img = saved.call_args[0][0]
self.assertEqual(img.size, (1024, 585)) # _fit preserves aspect ratio
if __name__ == "__main__":
unittest.main()