feat: original kitbashed hero assets built in Blender

First bespoke art authored for the project (tools/build_hero_assets.py,
headless Blender 5.1 -> assets/props/hero/):

- konbini.glb: 8m convenience-store front — fascia + teal sign band with
  ink trim, framed windows with sills and window ads, recessed double
  glass door, angled awning, drink machine, hedge planter. Placed on
  ~1/3 of wide buildings (glass-tagged collider)
- station.glb: viaduct station entrance — canopy on pillars, real stair
  steps with handrails and balusters, sign totem; placed at the central
  avenue crossing of the rail line
- clock.glb: 10m plaza clock tower — tiered base, ringed column, 4 clock
  faces with hands, teal crown; anchors the south-west plaza cell
- torii.glb: proper torii with segmented curved kasagi, shimaki, nuki
  beam and plaque — replaces the box torii in every shrine courtyard
  (invisible pillar colliders keep gameplay identical)

All hero pieces run through the toon pipeline with fine ink outlines.
Every part beveled; flat cel palette matches the city.

Verified: city renders clean with hero pieces placed; 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 03:00:17 -04:00
co-authored by Claude Fable 5
parent 6b3e363dfd
commit 7f9a579b63
6 changed files with 256 additions and 7 deletions
+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")