Feat/visual audio overhaul #21

Merged
Dotts merged 31 commits from feat/visual-audio-overhaul into main 2026-07-21 14:09:18 -07:00
6 changed files with 256 additions and 7 deletions
Showing only changes of commit 7f9a579b63 - Show all commits
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+39 -7
View File
@@ -96,6 +96,28 @@ func _kit_scene(key: String) -> PackedScene:
return _kit_cache[key]
# ── Hero assets (original kitbash, tools/build_hero_assets.py) ──────────────
## Hero piece with a box collider. Front faces -Z before yaw (glTF Y-up).
func _hero(key: String, pos: Vector3, yaw_deg: float, col_size: Vector3,
col_center_y: float, acoustic: String = "concrete") -> void:
var body := StaticBody3D.new()
body.name = "Hero_" + key + "_%d" % _rng.randi()
body.set_meta("acoustic_material", acoustic)
add_child(body)
body.global_position = pos
body.rotation_degrees.y = yaw_deg
var shape := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = col_size
shape.shape = bs
shape.position = Vector3(0, col_center_y, 0)
body.add_child(shape)
var inst: Node3D = _kit_scene_at("res://assets/props/hero/" + key + ".glb").instantiate()
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.015)
## Small visual-only kit prop (parasols, awnings): no collider needed.
func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> void:
var inst: Node3D = _kit_scene(key).instantiate()
@@ -177,8 +199,12 @@ func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: Str
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
# Storefront awning on some buildings (kit detail piece)
if _rng.randi() % 5 < 2:
# Kitbashed konbini storefront module on wide buildings
if w >= 12.0 and _rng.randi() % 3 == 0:
_hero("konbini", Vector3(front_x - float(dir) * 0.75, 0.02, zc), 90.0 * float(dir),
Vector3(8.2, 4.2, 1.4), 2.1, "glass")
# Storefront awning on some other buildings (kit detail piece)
elif _rng.randi() % 5 < 2:
_kit_prop_path(KIT_DIR + "detail-awning-wide.glb",
Vector3(front_x, 3.1, zc), face_yaw, minf(w * 0.9, 11.0))
# Street-level posters plastered on the facade (graphic density)
@@ -501,12 +527,11 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void:
_box_static(c + Vector3(0.0 if horiz else off, 0.7, off if horiz else 0.0),
Vector3(BLOCK - 6.0 if horiz else 0.6, 1.4, 0.6 if horiz else BLOCK - 6.0),
Color(0.55, 0.55, 0.52), n + "_Wall%d" % i, "brick")
# Torii gate at the south entry
# Kitbashed torii (curved kasagi) at the south entry; pillar colliders
var tz := c.z + (BLOCK - 6.0) * 0.5
_box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.8, 7.0, 0.8), VERMILION, n + "_PillarW", "wood")
_box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.8, 7.0, 0.8), VERMILION, n + "_PillarE", "wood")
_box_static(c + Vector3(0, 7.0, tz), Vector3(11.0, 0.7, 1.1), VERMILION, n + "_BeamTop", "wood")
_box_static(c + Vector3(0, 5.8, tz), Vector3(9.4, 0.5, 0.9), VERMILION.darkened(0.15), n + "_BeamLow", "wood")
_hero("torii", c + Vector3(0, 0.1, tz), 0.0, Vector3(0.1, 0.1, 0.1), 0.05, "wood")
_box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarW", "wood").visible = false
_box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarE", "wood").visible = false
# Hall with slide roof
_box_static(c + Vector3(0, 2.0, -10.0), Vector3(12.0, 4.0, 8.0), WOOD.darkened(0.2), n + "_Hall", "wood")
_ramp_static(c + Vector3(0, 4.9, -8.2), Vector3(13.0, 0.3, 5.6), Vector3(-24, 0, 0), VERMILION.darkened(0.35), n + "_Roof", "wood")
@@ -571,6 +596,9 @@ func _plaza_block(c: Vector3, bx: int, bz: int) -> void:
var p := c + Vector3(cos(a), 0, sin(a)) * (BLOCK * 0.32)
_box_static(Vector3(p.x, 0.55, p.z), Vector3(4.0, 1.1, 4.0), Color(0.5, 0.48, 0.52), "%s_Pl%d" % [n, i], "brick")
_kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 1.0, p.z), float(i) * 90.0, 4.5)
# Kitbashed clock tower on the south-west plaza cell
if bx == 4 and bz == 5:
_hero("clock", c + Vector3(0, 0.1, 0), 0.0, Vector3(3.2, 10.5, 3.2), 5.25, "concrete")
# The Crossing screen tower on the single centre-most plaza cell
if bx == 5 and bz == 4:
_box_static(c + Vector3(0, 9.0, 0), Vector3(6.0, 18.0, 6.0), METAL.darkened(0.2), n + "_ScreenTower", "metal")
@@ -637,6 +665,10 @@ func _build_rail_line() -> void:
_ad_quad(Vector3(x - 1.35, 2.0, z), Vector2(1.2, 1.8), -90.0, _poster_tex())
i += 1
x += PITCH
# Kitbashed station entrance beside the central avenue crossing
_hero("station", Vector3(ROAD_W * 0.5 + 8.0, 0.02, z + 7.5), 90.0,
Vector3(6.0, 3.6, 5.0), 1.8, "concrete")
# Slide ramps to the deck at the two main-avenue crossings
for sx in [-PITCH * 0.5 - 4.0, PITCH * 0.5 + 4.0]:
var ang := rad_to_deg(atan2(RAIL_TOP, 16.0))
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Kitbash original hero assets for Akiba Crossing in headless Blender.
Builds detailed modular pieces from beveled primitives with flat cel
palette materials (the game's toonify keeps base colors), origin at
ground center, sized in meters:
konbini.glb - 8m convenience-store front: fascia, sign band, framed
windows, recessed door, awning, drink machine, planter
station.glb - viaduct station entrance: canopy, pillars, real stairs,
handrails, sign totem
clock.glb - plaza clock tower: tiered base, column, 4 faces, ring
torii.glb - proper torii with curved kasagi beam and plaque
Run: blender --background --python tools/build_hero_assets.py
Outputs to assets/props/hero/.
"""
import math
import os
import sys
import bpy
OUT = os.path.join(os.getcwd(), "assets", "props", "hero")
os.makedirs(OUT, exist_ok=True)
_mats = {}
def mat(name, rgba):
if name in _mats:
return _mats[name]
m = bpy.data.materials.new(name)
m.use_nodes = True
bsdf = m.node_tree.nodes.get("Principled BSDF")
bsdf.inputs["Base Color"].default_value = (*rgba, 1.0)
bsdf.inputs["Roughness"].default_value = 0.9
_mats[name] = m
return m
INK = mat("ink", (0.07, 0.065, 0.09))
WHITE = mat("white", (0.85, 0.84, 0.82))
CREAM = mat("cream", (0.78, 0.74, 0.66))
RED = mat("red", (0.8, 0.2, 0.16))
VERMILION = mat("vermilion", (0.82, 0.24, 0.14))
TEAL = mat("teal", (0.16, 0.65, 0.65))
GLASS = mat("glass", (0.45, 0.68, 0.75))
METAL = mat("metal", (0.4, 0.44, 0.5))
WOOD = mat("wood", (0.42, 0.28, 0.16))
YELLOW = mat("yellow", (0.95, 0.75, 0.2))
GRAY = mat("gray", (0.55, 0.55, 0.58))
def clear():
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete()
def box(name, size, loc, material, bevel=0.02, rot=(0, 0, 0)):
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot)
o = bpy.context.active_object
o.name = name
o.scale = (size[0] / 2.0, size[1] / 2.0, size[2] / 2.0)
bpy.ops.object.transform_apply(scale=True)
if bevel > 0:
b = o.modifiers.new("bevel", "BEVEL")
b.width = bevel
b.segments = 2
b.limit_method = "ANGLE"
o.data.materials.append(material)
return o
def cyl(name, r, depth, loc, material, rot=(0, 0, 0), verts=24):
bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=r, depth=depth,
location=loc, rotation=rot)
o = bpy.context.active_object
o.name = name
o.data.materials.append(material)
return o
def export(path):
bpy.ops.object.select_all(action="SELECT")
bpy.ops.export_scene.gltf(filepath=path, use_selection=True)
print("hero: exported", path)
# ── Konbini storefront (8 x 4.2 x 1.4, front faces +Y) ───────────────────────
def konbini():
clear()
W, H, D = 8.0, 4.2, 1.2
# back panel + side returns
box("back", (W, 0.15, H), (0, -D / 2, H / 2), CREAM)
for sx in (-1, 1):
box("side", (0.3, D, H), (sx * (W / 2 - 0.15), 0, H / 2), CREAM)
# fascia + colored sign band with ink trim
box("fascia", (W, D, 0.5), (0, 0, H - 0.25), WHITE)
box("signband", (W - 0.3, D + 0.06, 0.7), (0, 0, H - 0.85), TEAL, 0.03)
box("trim_top", (W, D + 0.1, 0.08), (0, 0, H - 0.5), INK, 0.01)
box("trim_bot", (W, D + 0.1, 0.08), (0, 0, H - 1.2), INK, 0.01)
# framed windows left+right of the recessed door
for sx in (-1, 1):
cx = sx * (W / 4 + 0.35)
box("winframe", (2.6, 0.18, 2.2), (cx, D / 2 - 0.2, 1.35), INK, 0.015)
box("winglass", (2.3, 0.1, 1.9), (cx, D / 2 - 0.16, 1.35), GLASS, 0.0)
box("sill", (2.7, 0.35, 0.12), (cx, D / 2 - 0.25, 0.28), GRAY)
# window ad strip
box("winad", (1.0, 0.06, 0.5), (cx - 0.5, D / 2 - 0.08, 1.9), RED, 0.01)
# recessed doorway
box("doorback", (1.8, 0.1, 2.4), (0, -0.1, 1.2), INK, 0.0)
box("doorglassL", (0.8, 0.08, 2.1), (-0.45, 0.05, 1.15), GLASS, 0.0)
box("doorglassR", (0.8, 0.08, 2.1), (0.45, 0.05, 1.15), GLASS, 0.0)
box("doorframe", (2.0, 0.12, 0.15), (0, D / 2 - 0.35, 2.42), INK, 0.01)
# awning over the door
box("awning", (2.6, 0.9, 0.1), (0, D / 2 + 0.35, 2.6), RED, 0.02,
rot=(math.radians(12), 0, 0))
# drink machine + planter dressing
box("vending", (0.9, 0.7, 1.9), (W / 2 - 1.0, D / 2 - 0.3, 0.95), RED, 0.03)
box("vendglass", (0.7, 0.06, 1.0), (W / 2 - 1.0, D / 2 + 0.06, 1.3), GLASS, 0.0)
box("planter", (1.4, 0.5, 0.45), (-W / 2 + 1.1, D / 2 + 0.1, 0.225), GRAY, 0.03)
box("hedge", (1.25, 0.4, 0.35), (-W / 2 + 1.1, D / 2 + 0.1, 0.62), mat("leaf", (0.3, 0.55, 0.32)), 0.05)
export(os.path.join(OUT, "konbini.glb"))
# ── Station entrance (6 x 5 x 3.6, opening faces +Y) ─────────────────────────
def station():
clear()
W, D, H = 6.0, 5.0, 3.6
# canopy + ink edge
box("canopy", (W, D, 0.25), (0, 0, H), WHITE, 0.04)
box("canopy_edge", (W + 0.15, D + 0.15, 0.1), (0, 0, H - 0.16), INK, 0.01)
# pillars
for sx in (-1, 1):
for sy in (-1, 1):
cyl("pillar", 0.14, H, (sx * (W / 2 - 0.3), sy * (D / 2 - 0.3), H / 2), METAL)
# side walls (half height)
for sx in (-1, 1):
box("wall", (0.25, D, 1.2), (sx * (W / 2 - 0.125), 0, 0.6), CREAM)
# stairs descending toward -Y (6 steps down 1.8m)
steps = 6
for i in range(steps):
z = -0.15 - i * 0.3
y = -D / 2 + 0.55 + i * 0.7
box("step", (W - 1.2, 0.7, 0.3), (0, y, z), GRAY, 0.01)
# handrails along the stairwell
for sx in (-1, 1):
box("rail", (0.08, D - 0.8, 0.08),
(sx * (W / 2 - 0.7), 0.0, 0.95), INK, 0.0,
rot=(math.radians(-18), 0, 0))
for i in range(4):
box("baluster", (0.06, 0.06, 0.9),
(sx * (W / 2 - 0.7), -D / 2 + 0.8 + i * 1.1, 0.45 - i * 0.28), INK, 0.0)
# sign totem
box("totem", (0.5, 0.35, 4.4), (W / 2 + 0.5, D / 2 - 0.2, 2.2), METAL, 0.03)
box("totem_sign", (0.42, 0.1, 1.6), (W / 2 + 0.5, D / 2 - 0.01, 3.4), YELLOW, 0.01)
export(os.path.join(OUT, "station.glb"))
# ── Plaza clock tower (10m) ──────────────────────────────────────────────────
def clock():
clear()
box("base1", (3.2, 3.2, 0.5), (0, 0, 0.25), GRAY, 0.04)
box("base2", (2.4, 2.4, 0.5), (0, 0, 0.75), WHITE, 0.04)
cyl("column", 0.5, 6.5, (0, 0, 4.25), CREAM, verts=12)
for i in range(3):
cyl("colring", 0.58, 0.18, (0, 0, 1.6 + i * 2.4), INK, verts=12)
# head
box("head", (1.9, 1.9, 1.9), (0, 0, 8.4), WHITE, 0.06)
box("head_trim", (2.1, 2.1, 0.2), (0, 0, 7.45), INK, 0.02)
for i in range(4):
a = math.pi / 2 * i
dx, dy = math.cos(a), math.sin(a)
cyl("face", 0.68, 0.06, (dx * 0.98, dy * 0.98, 8.4), INK,
rot=(0, math.pi / 2, a))
cyl("face_in", 0.6, 0.08, (dx * 0.99, dy * 0.99, 8.4), WHITE,
rot=(0, math.pi / 2, a))
box("hand_h", (0.08, 0.08, 0.4), (dx * 1.05, dy * 1.05, 8.55), INK, 0.0)
box("hand_m", (0.08, 0.08, 0.28), (dx * 1.05 + dy * 0.12, dy * 1.05 + dx * 0.12, 8.32), INK, 0.0)
# crown
box("crown", (1.4, 1.4, 0.3), (0, 0, 9.5), TEAL, 0.03)
cyl("spire", 0.08, 1.2, (0, 0, 10.2), INK, verts=8)
export(os.path.join(OUT, "clock.glb"))
# ── Torii with curved kasagi (10 wide x 8 tall) ──────────────────────────────
def torii():
clear()
for sx in (-1, 1):
cyl("pillar", 0.42, 7.0, (sx * 4.0, 0, 3.5), VERMILION, verts=16)
cyl("pillar_base", 0.55, 0.5, (sx * 4.0, 0, 0.25), INK, verts=16)
# curved kasagi: segmented arc of boxes
segs = 9
for i in range(segs):
t = i / (segs - 1) - 0.5 # -0.5..0.5
x = t * 10.0
z = 7.3 + (1.0 - (2 * t) ** 2) * 0.45 # gentle arch
rot = (0, -t * 0.28, 0)
box("kasagi", (10.0 / segs + 0.15, 1.0, 0.55), (x, 0, z), INK, 0.02, rot=rot)
box("shimaki", (10.0 / segs + 0.15, 0.8, 0.35), (x, 0, z - 0.45), VERMILION, 0.02, rot=rot)
# nuki (lower tie beam) + plaque
box("nuki", (9.4, 0.55, 0.55), (0, 0, 5.6), VERMILION, 0.03)
box("gakuzuka", (0.5, 0.3, 1.1), (0, 0, 6.5), VERMILION, 0.02)
box("plaque", (0.9, 0.18, 1.3), (0, 0.2, 6.5), mat("plaque", (0.9, 0.86, 0.75)), 0.02)
export(os.path.join(OUT, "torii.glb"))
konbini()
station()
clock()
torii()
print("hero: all assets built")