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 zc := cursor + w * 0.5
|
||||||
var h: float = KIT_BUILDINGS[key].y * KIT_SCALE
|
var h: float = KIT_BUILDINGS[key].y * KIT_SCALE
|
||||||
_kit_building(key, Vector3(front_x, 0, zc), yaw, "K%s_%d" % [seed_id, idx])
|
_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
|
# Akiba signage layer, aligned to the model's measured bounds
|
||||||
if h >= 14.0:
|
if h >= 14.0:
|
||||||
var zs := cursor + 1.2
|
var zs := cursor + 1.2
|
||||||
var col_h := h * 0.55
|
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),
|
_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")
|
METAL, "K%s_%d_sc" % [seed_id, idx], "metal")
|
||||||
var segs := maxi(2, int(col_h / 3.4))
|
_ad_quad(Vector3(front_x - float(dir) * 0.84, h * 0.35, zs),
|
||||||
for s2 in segs:
|
Vector2(1.25, col_h * 0.9), face_yaw, _column_tex())
|
||||||
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)
|
|
||||||
if h >= 20.0:
|
if h >= 20.0:
|
||||||
var bw := minf(w - 4.0, 10.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),
|
_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")
|
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),
|
_ad_quad(Vector3(front_x + float(dir) * 1.7, h + 1.9, zc),
|
||||||
SIGN_COLORS[(idx + seed_id.hash()) % SIGN_COLORS.size()], true)
|
Vector2(bw - 0.6, 2.5), face_yaw, _board_tex())
|
||||||
cursor += w
|
cursor += w
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
@@ -210,6 +214,46 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
|
|||||||
return body
|
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:
|
func _deco_box(pos: Vector3, size: Vector3, color: Color, emissive: bool = false) -> void:
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
var bm := BoxMesh.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)
|
# Rooftop crown billboard both directions (skyline wayfinding)
|
||||||
var tw := dims.x * s
|
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")
|
_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)
|
_ad_quad(c + Vector3(0.42, h + 2.4, 0), Vector2(tw - 4.6, 3.3), 90.0, _board_tex())
|
||||||
_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())
|
||||||
_spawn_points.append(c + Vector3(0, FLOOR_H + 1.5, (BLOCK - 4.0) * 0.4))
|
_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")
|
_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:
|
for face in 4:
|
||||||
var a2 := TAU * float(face) / 4.0
|
var a2 := TAU * float(face) / 4.0
|
||||||
var off := Vector3(cos(a2), 0, sin(a2)) * 3.2
|
var off := Vector3(cos(a2), 0, sin(a2)) * 3.15
|
||||||
var flat := absf(off.x) > absf(off.z)
|
_ad_quad(c + off + Vector3(0, 11.0, 0), Vector2(4.6, 6.0),
|
||||||
_deco_box(c + off + Vector3(0, 11.0, 0),
|
rad_to_deg(a2) + 90.0, _board_tex())
|
||||||
Vector3(0.14 if flat else 4.6, 6.0, 4.6 if flat else 0.14),
|
|
||||||
SIGN_COLORS[face % SIGN_COLORS.size()], true)
|
|
||||||
_spawn_points.append(c + Vector3(BLOCK * 0.2, 1.5, -BLOCK * 0.2))
|
_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
|
var x := -CITY_HALF + PITCH * 0.5
|
||||||
while x <= CITY_HALF:
|
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")
|
_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:
|
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 + 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")
|
_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
|
i += 1
|
||||||
x += PITCH
|
x += PITCH
|
||||||
# Slide ramps to the deck at the two main-avenue crossings
|
# 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()
|
||||||