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:
co-authored by
Claude Fable 5
parent
6b3e363dfd
commit
7f9a579b63
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user