feat: ad-graphic layer — generated posters, billboards and sign columns

The city's flat color sign panels are now real graphic designs, closing
the street-level "graphic density" that anime cities live on:

- tools/generate_ads.py: procedural ad library (24 textures) — bold
  two-tone layouts (diagonal splits, bands, circles, stripes) with
  fake-glyph text blocks that read as kanji/kana from gameplay distance,
  inked borders, seeded palette system
- Every building gets 1-3 street-level posters plastered on its facade
- Vertical sign columns render a stacked-glyph column texture
- Rooftop billboards, tower crown boards (both faces) and the central
  4-face screen tower all show distinct ad graphics
- Posters under the viaduct arches on every pier
- All panels lit (albedo + emission of the same texture) so they glow
  as signage without blooming out

Verified via street capture (glyph posters, pharmacy-cross signs, ad
boards visible down the avenue); movement 11/11, smoke 0 failures,
acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-19 02:44:55 -04:00
co-authored by Claude Fable 5
parent 56db9eea59
commit 648872a16d
26 changed files with 184 additions and 15 deletions
+59 -15
View File
@@ -176,23 +176,27 @@ func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: Str
var zc := cursor + w * 0.5
var h: float = KIT_BUILDINGS[key].y * KIT_SCALE
_kit_building(key, Vector3(front_x, 0, zc), yaw, "K%s_%d" % [seed_id, idx])
var face_yaw := -90.0 if dir > 0 else 90.0 # ad quads face the street
# Street-level posters plastered on the facade (graphic density)
var posters := 1 + _rng.randi() % 3
for p in posters:
var pz := cursor + _rng.randf_range(1.5, maxf(w - 1.5, 2.0))
_ad_quad(Vector3(front_x - float(dir) * 0.15, _rng.randf_range(1.7, 2.4), pz),
Vector2(1.3, 2.0) * _rng.randf_range(0.8, 1.1), face_yaw, _poster_tex())
# Akiba signage layer, aligned to the model's measured bounds
if h >= 14.0:
var zs := cursor + 1.2
var col_h := h * 0.55
_box_static(Vector3(front_x - float(dir) * 0.45, h * 0.35, zs), Vector3(0.7, col_h, 1.4),
METAL, "K%s_%d_sc" % [seed_id, idx], "metal")
var segs := maxi(2, int(col_h / 3.4))
for s2 in segs:
var sy := h * 0.35 - col_h * 0.5 + col_h * (float(s2) + 0.5) / float(segs)
_deco_box(Vector3(front_x - float(dir) * 0.85, sy, zs), Vector3(0.12, 2.2, 1.2),
SIGN_COLORS[(s2 + idx + seed_id.hash()) % SIGN_COLORS.size()], true)
_ad_quad(Vector3(front_x - float(dir) * 0.84, h * 0.35, zs),
Vector2(1.25, col_h * 0.9), face_yaw, _column_tex())
if h >= 20.0:
var bw := minf(w - 4.0, 10.0)
_box_static(Vector3(front_x + float(dir) * 2.0, h + 1.9, zc), Vector3(0.5, 3.0, bw),
METAL, "K%s_%d_bf" % [seed_id, idx], "metal")
_deco_box(Vector3(front_x + float(dir) * 1.7, h + 1.9, zc), Vector3(0.12, 2.4, bw - 0.6),
SIGN_COLORS[(idx + seed_id.hash()) % SIGN_COLORS.size()], true)
_ad_quad(Vector3(front_x + float(dir) * 1.7, h + 1.9, zc),
Vector2(bw - 0.6, 2.5), face_yaw, _board_tex())
cursor += w
idx += 1
@@ -210,6 +214,46 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
return body
# ── Ad graphics (generated library: tools/generate_ads.py) ──────────────────
var _ad_mats: Dictionary = {}
func _ad_material(path: String) -> StandardMaterial3D:
if _ad_mats.has(path):
return _ad_mats[path]
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
var tex: Texture2D = load(path)
m.albedo_texture = tex
m.emission_enabled = true
m.emission_texture = tex
m.emission_energy_multiplier = 0.9
_ad_mats[path] = m
return m
## A lit ad panel (poster/billboard/sign face). Quad faces +Z before yaw.
func _ad_quad(pos: Vector3, size: Vector2, yaw_deg: float, tex_path: String) -> void:
var mi := MeshInstance3D.new()
var qm := QuadMesh.new()
qm.size = size
qm.material = _ad_material(tex_path)
mi.mesh = qm
add_child(mi)
mi.global_position = pos
mi.rotation_degrees.y = yaw_deg
func _poster_tex() -> String:
return "res://assets/textures/ads/ad_%02d.png" % (_rng.randi() % 10)
func _board_tex() -> String:
return "res://assets/textures/ads/board_%02d.png" % (_rng.randi() % 8)
func _column_tex() -> String:
return "res://assets/textures/ads/column_%02d.png" % (_rng.randi() % 6)
func _deco_box(pos: Vector3, size: Vector3, color: Color, emissive: bool = false) -> void:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
@@ -421,8 +465,8 @@ func _tower_block(c: Vector3, bx: int, bz: int) -> void:
# Rooftop crown billboard both directions (skyline wayfinding)
var tw := dims.x * s
_box_static(c + Vector3(0, h + 2.4, 0), Vector3(0.6, 4.0, tw - 4.0), METAL, n + "_Crown", "metal")
_deco_box(c + Vector3(0.45, h + 2.4, 0), Vector3(0.12, 3.4, tw - 4.6), SIGN_COLORS[(bx + bz) % SIGN_COLORS.size()], true)
_deco_box(c + Vector3(-0.45, h + 2.4, 0), Vector3(0.12, 3.4, tw - 4.6), SIGN_COLORS[(bx + bz + 2) % SIGN_COLORS.size()], true)
_ad_quad(c + Vector3(0.42, h + 2.4, 0), Vector2(tw - 4.6, 3.3), 90.0, _board_tex())
_ad_quad(c + Vector3(-0.42, h + 2.4, 0), Vector2(tw - 4.6, 3.3), -90.0, _board_tex())
_spawn_points.append(c + Vector3(0, FLOOR_H + 1.5, (BLOCK - 4.0) * 0.4))
@@ -501,11 +545,9 @@ func _plaza_block(c: Vector3, bx: int, bz: int) -> void:
_box_static(c + Vector3(0, 9.0, 0), Vector3(6.0, 18.0, 6.0), METAL.darkened(0.2), n + "_ScreenTower", "metal")
for face in 4:
var a2 := TAU * float(face) / 4.0
var off := Vector3(cos(a2), 0, sin(a2)) * 3.2
var flat := absf(off.x) > absf(off.z)
_deco_box(c + off + Vector3(0, 11.0, 0),
Vector3(0.14 if flat else 4.6, 6.0, 4.6 if flat else 0.14),
SIGN_COLORS[face % SIGN_COLORS.size()], true)
var off := Vector3(cos(a2), 0, sin(a2)) * 3.15
_ad_quad(c + off + Vector3(0, 11.0, 0), Vector2(4.6, 6.0),
rad_to_deg(a2) + 90.0, _board_tex())
_spawn_points.append(c + Vector3(BLOCK * 0.2, 1.5, -BLOCK * 0.2))
@@ -556,10 +598,12 @@ func _build_rail_line() -> void:
var x := -CITY_HALF + PITCH * 0.5
while x <= CITY_HALF:
_box_static(Vector3(x, (RAIL_TOP - 1.0) * 0.5, z), Vector3(2.2, RAIL_TOP - 1.0, 5.4), BRICK, "Rail_Pier%d" % i, "brick")
# vending pair under alternating arches
# vending pair + posters under alternating arches
if i % 2 == 0:
_box_static(Vector3(x + 4.0, 1.0, z + 1.8), Vector3(1.1, 2.0, 0.9), Color(0.85, 0.3, 0.3), "Rail_V%da" % i, "metal")
_box_static(Vector3(x + 5.6, 1.0, z + 1.8), Vector3(1.1, 2.0, 0.9), Color(0.3, 0.55, 0.85), "Rail_V%db" % i, "metal")
_ad_quad(Vector3(x + 1.35, 2.0, z), Vector2(1.2, 1.8), 90.0, _poster_tex())
_ad_quad(Vector3(x - 1.35, 2.0, z), Vector2(1.2, 1.8), -90.0, _poster_tex())
i += 1
x += PITCH
# Slide ramps to the deck at the two main-avenue crossings