Feat/visual audio overhaul #21
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate stylized ad/poster/sign textures for Akiba Crossing.
|
||||
|
||||
Bold two-tone layouts, punchy shapes and fake-glyph text blocks (random
|
||||
stroke clusters that read as kanji/kana from gameplay distance) — the
|
||||
graphic-design density of an anime city street, generated as a reusable
|
||||
library:
|
||||
|
||||
assets/textures/ads/ad_XX.png (vertical 256x512 posters/banners)
|
||||
assets/textures/ads/board_XX.png (horizontal 512x256 billboards)
|
||||
assets/textures/ads/column_XX.png (128x512 vertical sign columns)
|
||||
|
||||
Run: python tools/generate_ads.py
|
||||
"""
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
OUT = Path("assets/textures/ads")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
PALETTES = [
|
||||
[(238, 51, 94), (255, 255, 255), (24, 24, 32)],
|
||||
[(38, 206, 220), (24, 24, 40), (255, 255, 255)],
|
||||
[(255, 196, 37), (30, 30, 34), (238, 51, 94)],
|
||||
[(120, 90, 240), (255, 255, 255), (255, 196, 37)],
|
||||
[(46, 230, 120), (18, 24, 24), (255, 255, 255)],
|
||||
[(255, 255, 255), (238, 51, 94), (24, 24, 32)],
|
||||
[(24, 28, 44), (38, 206, 220), (255, 196, 37)],
|
||||
[(255, 120, 40), (255, 255, 255), (30, 30, 34)],
|
||||
]
|
||||
|
||||
rng = random.Random(20260719)
|
||||
|
||||
|
||||
def glyph(d: ImageDraw.ImageDraw, x, y, s, color):
|
||||
"""A fake kanji-ish glyph: a few thick strokes in an s x s box."""
|
||||
w = max(2, s // 7)
|
||||
strokes = rng.randint(2, 4)
|
||||
for _ in range(strokes):
|
||||
kind = rng.randint(0, 2)
|
||||
if kind == 0: # horizontal bar
|
||||
yy = y + rng.randint(0, s - w)
|
||||
d.rectangle([x, yy, x + s, yy + w], fill=color)
|
||||
elif kind == 1: # vertical bar
|
||||
xx = x + rng.randint(0, s - w)
|
||||
d.rectangle([xx, y, xx + w, y + s], fill=color)
|
||||
else: # box
|
||||
m = rng.randint(s // 5, s // 3)
|
||||
d.rectangle([x + m, y + m, x + s - m, y + s - m], outline=color, width=w)
|
||||
|
||||
|
||||
def text_block(d, x, y, s, count, color, vertical=True):
|
||||
for i in range(count):
|
||||
if vertical:
|
||||
glyph(d, x, y + i * int(s * 1.25), s, color)
|
||||
else:
|
||||
glyph(d, x + i * int(s * 1.25), y, s, color)
|
||||
|
||||
|
||||
def background(d, w, h, pal):
|
||||
d.rectangle([0, 0, w, h], fill=pal[0])
|
||||
layout = rng.randint(0, 3)
|
||||
if layout == 0: # diagonal split
|
||||
d.polygon([(0, h), (w, h), (w, rng.randint(0, h // 2))], fill=pal[1])
|
||||
elif layout == 1: # bottom band
|
||||
d.rectangle([0, int(h * 0.72), w, h], fill=pal[1])
|
||||
elif layout == 2: # big circle
|
||||
r = int(min(w, h) * 0.42)
|
||||
cx, cy = rng.randint(r, w - r), rng.randint(r, h - r)
|
||||
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=pal[1])
|
||||
else: # stripes
|
||||
n = rng.choice([3, 4])
|
||||
for i in range(n):
|
||||
if i % 2 == 0:
|
||||
d.rectangle([i * w // n, 0, (i + 1) * w // n, h], fill=pal[1])
|
||||
|
||||
|
||||
def poster(path, w, h):
|
||||
pal = rng.choice(PALETTES)
|
||||
img = Image.new("RGB", (w, h))
|
||||
d = ImageDraw.Draw(img)
|
||||
background(d, w, h, pal)
|
||||
ink = pal[2]
|
||||
if h > w: # vertical: glyph column + accent dot
|
||||
s = w // 4
|
||||
text_block(d, w - s - s // 2, s // 2, s, rng.randint(3, 5), ink, vertical=True)
|
||||
if rng.random() < 0.6:
|
||||
r = w // 6
|
||||
d.ellipse([s // 2, h - 2 * r - s // 2, s // 2 + 2 * r, h - s // 2], fill=ink)
|
||||
else: # horizontal: glyph row
|
||||
s = h // 3
|
||||
text_block(d, s // 2, (h - s) // 2, s, rng.randint(3, 6), ink, vertical=False)
|
||||
# border
|
||||
d.rectangle([0, 0, w - 1, h - 1], outline=(15, 14, 20), width=max(3, w // 60))
|
||||
img.save(path)
|
||||
|
||||
|
||||
def column(path, w, h):
|
||||
"""Vertical sign column: stacked glyphs, one per segment."""
|
||||
pal = rng.choice(PALETTES)
|
||||
img = Image.new("RGB", (w, h))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rectangle([0, 0, w, h], fill=pal[0])
|
||||
s = int(w * 0.6)
|
||||
n = h // int(s * 1.4)
|
||||
for i in range(n):
|
||||
glyph(d, (w - s) // 2, int(i * s * 1.4 + s * 0.25), s, pal[2])
|
||||
d.rectangle([0, 0, w - 1, h - 1], outline=(15, 14, 20), width=3)
|
||||
img.save(path)
|
||||
|
||||
|
||||
def main():
|
||||
for i in range(10):
|
||||
poster(OUT / f"ad_{i:02d}.png", 256, 512)
|
||||
for i in range(8):
|
||||
poster(OUT / f"board_{i:02d}.png", 512, 256)
|
||||
for i in range(6):
|
||||
column(OUT / f"column_{i:02d}.png", 128, 512)
|
||||
print(f"generated {10 + 8 + 6} ad textures in {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||