Use the photo-anchored ashlar limestone on the masonic facade
No files matched your search
@@ -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` →
|
||||
retopo/UV assist → atlas → `audit`), and document the exact commands in
|
||||
`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),
|
||||
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.
|
||||
(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
|
||||
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.
|
||||
|
||||
|
After Width: | Height: | Size: 639 KiB |
|
After Width: | Height: | Size: 501 KiB |
@@ -73,6 +73,10 @@ not texture or geometry sources.
|
||||
- Local lights use linear attenuation (`omni_attenuation = 1.0`). Godot treats
|
||||
`0.0` as constant falloff, which previously turned the night facade into a
|
||||
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
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@ attribution lives in the caption and the [refs sidecars](../refs/README.md)).
|
||||
|
||||
| Board | Caption |
|
||||
| --- | --- |
|
||||
|  | 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. |
|
||||
|  | 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). |
|
||||
|  | 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. |
|
||||
|  | Joke Shop frontage, day. Photo: public domain (2006) via `refs/joke-shop`. Concept: generated. Capture: current build. |
|
||||
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 174 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"concept": "docs/screens/concepts/hollywood_boulevard/hollywood_boulevard_entrance_day-v2.png",
|
||||
"capture": "docs/screenshots/01-venue-overview.png"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 542 KiB After Width: | Height: | Size: 520 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 609 KiB |
|
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 757 KiB After Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 403 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 841 KiB After Width: | Height: | Size: 841 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -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_WET_NORMAL := preload(PICK_DIR + "asphalt_wet_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
|
||||
## 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:
|
||||
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),
|
||||
wet: bool = false) -> StandardMaterial3D:
|
||||
var texture: Texture2D = T_ASPHALT_WET if wet else T_ASPHALT
|
||||
|
||||
@@ -9,6 +9,7 @@ const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
|
||||
const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
|
||||
const T_AWNING := preload(PICK_DIR + "awning_canvas.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
|
||||
## 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")
|
||||
@@ -86,8 +87,10 @@ const VENUE_SPECS := {
|
||||
## a recessed central entrance and two street-level side doors.
|
||||
"architecture": "masonic",
|
||||
"facade_width": 29.6, "facade_height": 14.5, "facade_depth": 8.0,
|
||||
"facade_center_z": -8.0, "facade_texture": T_LIMESTONE,
|
||||
"facade_tint": Color(0.88, 0.86, 0.82), "facade_role": "stone",
|
||||
"facade_center_z": -8.0, "facade_texture": T_ASHLAR,
|
||||
# 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": [],
|
||||
"stair_width": 12.0, "stair_count": 8, "stair_tread": 0.38, "stair_height": 0.10,
|
||||
"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
|
||||
# limestone-like render; granite/limestone keeps the block consistent with
|
||||
# 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),
|
||||
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"
|
||||
# Rusticated base and string courses.
|
||||
_add_box(building, Vector3(w + 0.3, 1.0, d + 0.3), Vector3(0, 0.5, cz + 0.12),
|
||||
|
||||
@@ -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,
|
||||
transparent: bool = False) -> None:
|
||||
"""Generate one texture via OpenRouter and save it under assets/textures/."""
|
||||
img = generate_image(prompt, model, size, transparent)
|
||||
transparent: bool = False,
|
||||
reference_path=None) -> None:
|
||||
"""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)
|
||||
if transparent:
|
||||
img = img.convert("RGBA")
|
||||
@@ -556,7 +568,11 @@ def _run_ai(args: argparse.Namespace) -> None:
|
||||
if args.prompt:
|
||||
if not args.name:
|
||||
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
|
||||
for name, prompt, transparent in AI_TEXTURES:
|
||||
print(f"generating {name} ...")
|
||||
@@ -579,6 +595,14 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
"--model", 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(
|
||||
"--size", type=int, default=GEN_SIZE,
|
||||
help=f"square size requested from the API in px (min {GEN_SIZE}); "
|
||||
|
||||
@@ -230,6 +230,7 @@ PICKS = {
|
||||
"skyline_night": "skyline_night-v4",
|
||||
"brick_facade": "brick_facade",
|
||||
"stone_steps": "stone_steps-v2",
|
||||
"limestone_ashlar": "limestone_ashlar",
|
||||
"sidewalk_pavers": "sidewalk_pavers",
|
||||
"asphalt": "asphalt",
|
||||
"asphalt_wet": "asphalt_wet",
|
||||
|
||||
@@ -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()
|
||||