#!/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") # ── Batch 2: street furniture ──────────────────────────────────────────────── def yatai(): """Ramen cart: body, roof, noren curtain strips, stools, lantern.""" clear() box("body", (2.6, 1.2, 1.1), (0, 0, 0.75), WOOD, 0.03) box("counter", (2.8, 1.35, 0.08), (0, 0, 1.34), mat("counter", (0.62, 0.45, 0.28)), 0.02) for sx in (-1, 1): box("post", (0.08, 0.08, 1.3), (sx * 1.2, -0.45, 2.0), INK, 0.0) cyl("wheel", 0.28, 0.1, (sx * 0.9, 0.62, 0.28), INK, rot=(math.radians(90), 0, 0)) box("roof", (3.0, 1.5, 0.12), (0, 0, 2.7), RED, 0.02) box("roof_trim", (3.1, 1.6, 0.06), (0, 0, 2.62), INK, 0.0) # noren curtain strips across the front for i in range(5): x = -1.1 + i * 0.55 box("noren", (0.42, 0.04, 0.6), (x, 0.72, 2.28), mat("noren", (0.92, 0.9, 0.85)), 0.0) box("noren_bar", (2.7, 0.05, 0.05), (0, 0.72, 2.6), INK, 0.0) for i, x in enumerate((-0.9, 0.0, 0.9)): cyl("stool", 0.18, 0.5, (x, 1.05, 0.25), RED, verts=12) cyl("lantern", 0.16, 0.42, (1.35, 0.7, 2.3), mat("lamp", (0.98, 0.62, 0.25)), verts=12) export(os.path.join(OUT, "yatai.glb")) def koban(): """Police box: small building, sign board, red lamp.""" clear() W, D, H = 3.2, 3.0, 3.2 box("body", (W, D, H), (0, 0, H / 2), CREAM, 0.03) box("roof", (W + 0.5, D + 0.5, 0.3), (0, 0, H + 0.15), TEAL, 0.03) box("door", (0.95, 0.1, 2.1), (-0.6, D / 2, 1.05), INK, 0.01) box("window", (1.1, 0.1, 1.0), (0.7, D / 2, 1.7), GLASS, 0.01) box("winframe", (1.25, 0.06, 1.15), (0.7, D / 2 - 0.02, 1.7), INK, 0.01) box("sign", (1.6, 0.12, 0.5), (0, D / 2 + 0.1, 2.75), WHITE, 0.01) cyl("lamp", 0.12, 0.24, (0, D / 2 + 0.2, 3.05), RED, verts=10) export(os.path.join(OUT, "koban.glb")) def phonebooth(): clear() box("frame", (1.1, 1.1, 2.5), (0, 0, 1.25), mat("booth", (0.2, 0.45, 0.3)), 0.03) for a in range(4): ang = math.pi / 2 * a dx, dy = math.cos(ang), math.sin(ang) box("glass", (0.06 if a % 2 == 0 else 0.85, 0.85 if a % 2 == 0 else 0.06, 1.6), (dx * 0.53, dy * 0.53, 1.35), GLASS, 0.0) box("cap", (1.25, 1.25, 0.18), (0, 0, 2.6), INK, 0.02) export(os.path.join(OUT, "phonebooth.glb")) def busstop(): clear() box("bench", (2.6, 0.5, 0.1), (0, 0.2, 0.55), WOOD, 0.02) for sx in (-1, 1): box("leg", (0.1, 0.45, 0.55), (sx * 1.1, 0.2, 0.27), INK, 0.0) box("cpost", (0.1, 0.1, 2.4), (sx * 1.5, -0.25, 1.2), METAL, 0.0) box("croof", (3.6, 1.3, 0.1), (0, 0.15, 2.45), GLASS, 0.02) box("croof_trim", (3.7, 1.4, 0.05), (0, 0.15, 2.38), INK, 0.0) box("signpole", (0.08, 0.08, 3.0), (2.1, -0.3, 1.5), METAL, 0.0) cyl("signdisc", 0.35, 0.06, (2.1, -0.3, 2.85), YELLOW, rot=(math.radians(90), 0, 0), verts=16) export(os.path.join(OUT, "busstop.glb")) def bicycle(): clear() for wy in (-0.55, 0.55): bpy.ops.mesh.primitive_torus_add(major_radius=0.32, minor_radius=0.035, location=(0, wy, 0.32), rotation=(math.radians(90), 0, math.radians(90))) o = bpy.context.active_object o.data.materials.append(INK) box("frame1", (0.05, 0.7, 0.05), (0, 0.1, 0.55), RED, 0.0, rot=(math.radians(25), 0, 0)) box("frame2", (0.05, 0.6, 0.05), (0, -0.2, 0.5), RED, 0.0, rot=(math.radians(-35), 0, 0)) box("bar", (0.4, 0.05, 0.05), (0, -0.52, 0.85), INK, 0.0) box("seat", (0.14, 0.3, 0.06), (0, 0.28, 0.85), INK, 0.01) box("basket", (0.32, 0.3, 0.25), (0, -0.72, 0.72), METAL, 0.01) export(os.path.join(OUT, "bicycle.glb")) yatai() koban() phonebooth() busstop() bicycle() print("hero: batch 2 built") # ── Batch 3: footbridge + station platform ─────────────────────────────────── def footbridge(): """Pedestrian bridge: 18m deck, X-trusses, railings, stairs both ends.""" clear() SPAN, DH = 18.0, 5.5 box("deck", (SPAN, 3.0, 0.35), (0, 0, DH), GRAY, 0.03) box("deck_edge", (SPAN + 0.2, 3.2, 0.12), (0, 0, DH - 0.22), INK, 0.01) # side trusses: X pattern of rotated bars + top chord for sy in (-1, 1): y = sy * 1.45 box("chord", (SPAN, 0.12, 0.12), (0, y, DH + 1.25), TEAL, 0.01) n = 6 for i in range(n): x = -SPAN / 2 + SPAN / n * (i + 0.5) for ang in (0.6, -0.6): box("truss", (0.09, 0.09, 1.55), (x, y, DH + 0.62), TEAL, 0.0, rot=(0, ang, 0)) # rail mesh box("rail", (SPAN, 0.05, 0.5), (0, y, DH + 0.55), mat("railmesh", (0.3, 0.32, 0.38)), 0.0) # stairs at both ends, descending outward along the bridge axis steps = 10 for end in (-1, 1): for i in range(steps): z = DH - 0.3 - i * (DH - 0.3) / steps x = end * (SPAN / 2 + 0.5 + i * 0.62) box("step", (0.62, 2.6, 0.22), (x, 0, z), GRAY, 0.01) # stair rails box("stairrail", (steps * 0.64, 0.08, 0.08), (end * (SPAN / 2 + 0.4 + steps * 0.31), 1.25, DH * 0.55), INK, 0.0, rot=(0, end * -0.68, 0)) export(os.path.join(OUT, "footbridge.glb")) def platform(): """Station platform strip for the viaduct deck: canopy, benches, signs.""" clear() L = 14.0 for i in range(4): x = -L / 2 + L / 4 * (i + 0.5) cyl("post", 0.1, 2.6, (x, 0, 1.3), METAL) box("proof", (L, 2.2, 0.12), (0, 0, 2.7), WHITE, 0.02) box("proof_edge", (L + 0.2, 2.4, 0.06), (0, 0, 2.6), TEAL, 0.01) for x in (-L / 4, L / 4): box("bench", (1.8, 0.45, 0.08), (x, 0.4, 0.5), WOOD, 0.01) for sx in (-1, 1): box("bleg", (0.08, 0.4, 0.5), (x + sx * 0.75, 0.4, 0.25), INK, 0.0) box("psign", (2.2, 0.1, 0.55), (0, 0.9, 2.15), mat("psign", (0.15, 0.35, 0.6)), 0.01) export(os.path.join(OUT, "platform.glb")) footbridge() platform() print("hero: batch 3 built")