feat: detailed building models across the city (Kenney City Kit, CC0)

Boxes with lines can't reach the requested fidelity — the city now uses
real modeled architecture, toonified into the cel style:

- Kenney City Kit Commercial (CC0, 41 GLBs) installed at
  assets/props/citykit: 13 mid-rise buildings + 5 glass skyscrapers with
  actual modeled window insets, doors, storefronts, awnings, trim and
  rooftop detail, plus parasol/awning props
- Standard + market blocks: street edges fill with seeded kit buildings
  (measured catalog drives placement, x13 scale -> 11-32m facades) over
  box colliders; Akiba signage layer (corner sign columns, glowing
  segments, roof billboards) attaches aligned to each model's bounds
- Tower blocks: kit skyscrapers (up to ~70m curtain-wall glass) on the
  walkable podium, seeded rotation, crown billboards kept
- Market blocks add cafe parasols among the stalls
- SSAO enabled + exposure recalibrated for the kit's bright albedo
  (sun 2.2 / ambient 1.7); lighter asphalt/paving to match; lighter haze

All kit meshes run through apply_toon_recursive: toon banding over the
kit textures, matching characters/weapons/VFX.

Verified: street + aerial captures (detailed facades at eye level, dense
believable skyline); movement 11/11, smoke 0, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-18 14:59:52 -04:00
co-authored by Claude Fable 5
parent b0af2ea9ee
commit 67040ba12d
44 changed files with 188 additions and 46 deletions
+28
View File
@@ -0,0 +1,28 @@
City Kit Commercial (2.1)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 21-07-2025 18:16
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
You can use this content for personal, educational, and commercial purposes.
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
------------------------------
• Website : www.kenney.nl
• Donate : www.kenney.nl/donate
• Patreon : patreon.com/kenney
Follow on social media for updates:
• Twitter: twitter.com/KenneyNL
• BlueSky: kenney.bsky.social
• Instagram: instagram.com/kenney_nl
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+160 -46
View File
@@ -42,8 +42,8 @@ const LAYOUT: Array[String] = [
] ]
# Palette # Palette
const ASPHALT := Color(0.3, 0.3, 0.37) const ASPHALT := Color(0.38, 0.38, 0.45)
const PAVING := Color(0.47, 0.45, 0.51) const PAVING := Color(0.56, 0.54, 0.6)
const STRIPE := Color(0.72, 0.72, 0.75) const STRIPE := Color(0.72, 0.72, 0.75)
const FACADES: Array[Color] = [ const FACADES: Array[Color] = [
Color(0.74, 0.71, 0.66), Color(0.62, 0.63, 0.67), Color(0.68, 0.6, 0.5), Color(0.74, 0.71, 0.66), Color(0.62, 0.63, 0.67), Color(0.68, 0.6, 0.5),
@@ -69,6 +69,117 @@ const SIGN_COLORS: Array[Color] = [
var _spawn_points: Array[Vector3] = [] var _spawn_points: Array[Vector3] = []
var _rng := RandomNumberGenerator.new() var _rng := RandomNumberGenerator.new()
# ── Kenney City Kit catalog (CC0) — base sizes measured at import scale ─────
const KIT_DIR := "res://assets/props/citykit/"
const KIT_SCALE := 13.0
## name -> Vector3(facade_width, height, depth) at import scale
const KIT_BUILDINGS := {
"building-a": Vector3(0.884, 1.293, 0.94), "building-b": Vector3(0.97, 1.293, 0.94),
"building-c": Vector3(0.884, 0.893, 1.09), "building-d": Vector3(0.84, 1.293, 0.9),
"building-e": Vector3(1.64, 0.893, 1.008), "building-f": Vector3(0.84, 1.693, 1.03),
"building-g": Vector3(0.97, 1.693, 0.922), "building-h": Vector3(0.884, 1.293, 1.008),
"building-i": Vector3(1.24, 1.68, 1.302), "building-j": Vector3(2.084, 1.693, 1.34),
"building-k": Vector3(2.084, 1.47, 0.942), "building-l": Vector3(1.37, 2.27, 1.402),
"building-n": Vector3(2.32, 2.48, 1.82),
}
const KIT_SKYSCRAPERS := {
"building-skyscraper-a": Vector3(1.36, 2.88, 1.36), "building-skyscraper-b": Vector3(1.36, 4.48, 1.36),
"building-skyscraper-c": Vector3(1.28, 4.08, 1.388), "building-skyscraper-d": Vector3(1.28, 5.47, 1.388),
"building-skyscraper-e": Vector3(1.295, 4.08, 1.242),
}
var _kit_cache: Dictionary = {}
func _kit_scene(key: String) -> PackedScene:
if not _kit_cache.has(key):
_kit_cache[key] = load(KIT_DIR + key + ".glb")
return _kit_cache[key]
## 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()
inst.scale = Vector3.ONE * scale_f
add_child(inst)
inst.global_position = pos
inst.rotation_degrees.y = yaw_deg
LevelMaterials.apply_toon_recursive(inst, 0.02)
## Place a kit model with a box collider matching its scaled bounds.
## front_pos: centre of the facade at ground level. yaw_deg: 0 faces +Z.
func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: String,
scale_f: float = KIT_SCALE) -> float:
var dims: Vector3 = KIT_BUILDINGS.get(key, KIT_SKYSCRAPERS.get(key, Vector3.ONE))
var w := dims.x * scale_f
var h := dims.y * scale_f
var d := dims.z * scale_f
var fwd := Vector3(sin(deg_to_rad(yaw_deg)), 0, cos(deg_to_rad(yaw_deg)))
var center := front_pos - fwd * d * 0.5
var body := StaticBody3D.new()
body.name = node_name
body.set_meta("acoustic_material", "concrete")
add_child(body)
body.global_position = center
body.rotation_degrees.y = yaw_deg
var shape := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = Vector3(w, h, d)
shape.shape = bs
shape.position = Vector3(0, h * 0.5, 0)
body.add_child(shape)
var inst: Node3D = _kit_scene(key).instantiate()
inst.scale = Vector3.ONE * scale_f
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.0)
return w
## Fill one street-facing edge of a lot with kit buildings + Akiba signage.
func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: String) -> void:
var yaw := -90.0 if dir > 0 else 90.0 # face the street (-x for east row)
var cursor := z0
var idx := 0
var keys := KIT_BUILDINGS.keys()
while z1 - cursor > 10.0:
var key: String = keys[_rng.randi() % keys.size()]
var w: float = KIT_BUILDINGS[key].x * KIT_SCALE
if w > z1 - cursor:
# find any model that still fits, else stop
var fits := false
for k2 in keys:
if KIT_BUILDINGS[k2].x * KIT_SCALE <= z1 - cursor:
key = k2
w = KIT_BUILDINGS[k2].x * KIT_SCALE
fits = true
break
if not fits:
break
var zc := cursor + w * 0.5
var h: float = KIT_BUILDINGS[key].y * KIT_SCALE
_kit_building(key, Vector3(front_x, 0, zc), yaw, "K%s_%d" % [seed_id, idx])
# Akiba signage layer, aligned to the model's measured bounds
if h >= 14.0:
var zs := cursor + 1.2
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),
METAL, "K%s_%d_sc" % [seed_id, idx], "metal")
var segs := maxi(2, int(col_h / 3.4))
for s2 in segs:
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:
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),
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),
SIGN_COLORS[(idx + seed_id.hash()) % SIGN_COLORS.size()], true)
cursor += w
idx += 1
# ── Cel-styled statics + render-only decoration ────────────────────────────── # ── Cel-styled statics + render-only decoration ──────────────────────────────
@@ -106,13 +217,18 @@ func _deco_box(pos: Vector3, size: Vector3, color: Color, emissive: bool = false
func _build_environment() -> void: func _build_environment() -> void:
var env := LevelEnvironment.add_to(self, "sunset") var env := LevelEnvironment.add_to(self, "sunset")
# Calibrated for the kit's bright albedo textures (they clip to white at
# the energies the old dark flat palette needed).
env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.environment.ambient_light_color = Color(0.58, 0.54, 0.66) env.environment.ambient_light_color = Color(0.58, 0.54, 0.66)
env.environment.ambient_light_energy = 3.0 env.environment.ambient_light_energy = 1.7
env.environment.fog_density = 0.0011 # city haze for depth over 600m env.environment.fog_density = 0.0007 # light city haze for depth over 600m
# Ambient occlusion grounds the modeled facade detail (ZZZ-style depth)
env.environment.ssao_enabled = true
env.environment.ssao_intensity = 2.0
var sun := get_node_or_null("Sun") var sun := get_node_or_null("Sun")
if sun: if sun:
sun.light_energy = 3.0 sun.light_energy = 2.2
sun.rotation_degrees = Vector3(-38, 55, 0) sun.rotation_degrees = Vector3(-38, 55, 0)
sun.directional_shadow_max_distance = 220.0 sun.directional_shadow_max_distance = 220.0
@@ -241,22 +357,11 @@ func _building(front_x: float, dir: int, z0: float, z1: float, depth: float,
## escapes (wallrun + climb route). ## escapes (wallrun + climb route).
func _standard_block(c: Vector3, bx: int, bz: int) -> void: func _standard_block(c: Vector3, bx: int, bz: int) -> void:
var half := BLOCK * 0.5 var half := BLOCK * 0.5
var alley := _rng.randi() % 2 == 0 # Detailed kit buildings fill both street-facing edges
var gap := 4.0 if alley else 0.0 _kit_fill_side(c.x - half, 1, c.z - half, c.z + half, "%d_%dW" % [bx, bz])
var depth := (BLOCK - gap) * 0.5 _kit_fill_side(c.x + half, -1, c.z - half, c.z + half, "%d_%dE" % [bx, bz])
for side in 2: # Fire escapes into the mid-block gap on some blocks
var dirv := 1 if side == 0 else -1 # 0: faces west street, body +x if _rng.randi() % 2 == 0:
var front := c.x - float(dirv) * half
# Two buildings along z per face
var split := c.z + _rng.randf_range(-6.0, 6.0)
var f1 := 2 + _rng.randi() % 4
var f2 := 2 + _rng.randi() % 4
var col1 := FACADES[_rng.randi() % FACADES.size()]
var col2 := FACADES[_rng.randi() % FACADES.size()]
_building(front, dirv, c.z - half, split, depth, f1, col1, "%d_%d_%d0" % [bx, bz, side])
_building(front, dirv, split, c.z + half, depth, f2, col2, "%d_%d_%d1" % [bx, bz, side])
if alley:
# Fire escapes climbing into the mid-block alley
for f in range(1, 3): for f in range(1, 3):
var y := float(f) * FLOOR_H - 0.4 var y := float(f) * FLOOR_H - 0.4
_box_static(c + Vector3(-2.0 + float(f) * 1.3, y, 4.0), Vector3(2.2, 0.16, 2.2), METAL, _box_static(c + Vector3(-2.0 + float(f) * 1.3, y, 4.0), Vector3(2.2, 0.16, 2.2), METAL,
@@ -267,29 +372,36 @@ func _standard_block(c: Vector3, bx: int, bz: int) -> void:
## Landmark: an 8-10 floor tower with a retail podium and mega-billboards. ## Landmark: an 8-10 floor tower with a retail podium and mega-billboards.
func _tower_block(c: Vector3, bx: int, bz: int) -> void: func _tower_block(c: Vector3, bx: int, bz: int) -> void:
var floors := 8 + _rng.randi() % 3
var h := float(floors) * FLOOR_H
var col := SEGA_RED if _rng.randi() % 3 == 0 else (ELEC_BLUE if _rng.randi() % 2 == 0 else FACADES[_rng.randi() % FACADES.size()])
var n := "Tower_%d_%d" % [bx, bz] var n := "Tower_%d_%d" % [bx, bz]
# Podium (1 floor, walkable roof ring) var col := SEGA_RED if _rng.randi() % 3 == 0 else (ELEC_BLUE if _rng.randi() % 2 == 0 else FACADES[_rng.randi() % FACADES.size()])
# Podium (1 floor, walkable roof ring) with glass front
_box_static(c + Vector3(0, FLOOR_H * 0.5, 0), Vector3(BLOCK - 4.0, FLOOR_H, BLOCK - 4.0), col.darkened(0.15), n + "_Podium", "concrete") _box_static(c + Vector3(0, FLOOR_H * 0.5, 0), Vector3(BLOCK - 4.0, FLOOR_H, BLOCK - 4.0), col.darkened(0.15), n + "_Podium", "concrete")
_box_static(c + Vector3(0, 2.0, -(BLOCK - 4.0) * 0.5 + 0.1), Vector3(BLOCK - 8.0, 3.0, 0.25), GLASS, n + "_Glass", "glass") _box_static(c + Vector3(0, 2.0, -(BLOCK - 4.0) * 0.5 + 0.1), Vector3(BLOCK - 8.0, 3.0, 0.25), GLASS, n + "_Glass", "glass")
# Tower shaft # Detailed kit skyscraper on the podium
var tw := BLOCK - 20.0 var keys := KIT_SKYSCRAPERS.keys()
_box_static(c + Vector3(0, FLOOR_H + (h - FLOOR_H) * 0.5, 0), Vector3(tw, h - FLOOR_H, tw), col, n, "concrete") var key: String = keys[_rng.randi() % keys.size()]
# Window + sign bands on two faces var dims: Vector3 = KIT_SKYSCRAPERS[key]
for f in range(2, floors): var s := (BLOCK - 22.0) / dims.x # shaft footprint ~26m
var y := float(f) * FLOOR_H var h := dims.y * s + FLOOR_H
_deco_box(c + Vector3(-tw * 0.5 - 0.1, y + 1.7, 0), Vector3(0.14, 2.0, tw - 3.0), WINDOW) var yaw := float(90 * (_rng.randi() % 4))
_deco_box(c + Vector3(tw * 0.5 + 0.1, y + 1.7, 0), Vector3(0.14, 2.0, tw - 3.0), WINDOW) var body := StaticBody3D.new()
# Mega vertical sign on the tower corner body.name = n
_box_static(c + Vector3(-tw * 0.5 - 0.5, FLOOR_H + (h - FLOOR_H) * 0.45, -tw * 0.5 + 1.5), body.set_meta("acoustic_material", "concrete")
Vector3(0.8, (h - FLOOR_H) * 0.8, 1.6), METAL, n + "_SignCol", "metal") add_child(body)
for s2 in floors - 2: body.global_position = c + Vector3(0, FLOOR_H, 0)
var sy := FLOOR_H * 2.0 + (h - FLOOR_H * 3.0) * float(s2) / float(maxi(floors - 3, 1)) body.rotation_degrees.y = yaw
_deco_box(c + Vector3(-tw * 0.5 - 0.95, sy, -tw * 0.5 + 1.5), Vector3(0.14, 2.4, 1.4), var shape := CollisionShape3D.new()
SIGN_COLORS[s2 % SIGN_COLORS.size()], true) var bs := BoxShape3D.new()
# Rooftop crown billboard both directions bs.size = Vector3(dims.x * s, dims.y * s, dims.z * s)
shape.shape = bs
shape.position = Vector3(0, dims.y * s * 0.5, 0)
body.add_child(shape)
var inst: Node3D = _kit_scene(key).instantiate()
inst.scale = Vector3.ONE * s
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.0)
# Rooftop crown billboard both directions (skyline wayfinding)
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) _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)
_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) _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)
@@ -330,11 +442,13 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void:
## Market street: two dense stall rows with lantern strings — low cover maze. ## Market street: two dense stall rows with lantern strings — low cover maze.
func _market_block(c: Vector3, bx: int, bz: int) -> void: func _market_block(c: Vector3, bx: int, bz: int) -> void:
var n := "Mkt_%d_%d" % [bx, bz] var n := "Mkt_%d_%d" % [bx, bz]
# Back walls: modest 2-floor buildings on east/west faces # Back walls: detailed kit buildings on east/west faces
_building(c.x - BLOCK * 0.5, 1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, 10.0, 2, _kit_fill_side(c.x - BLOCK * 0.5, 1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, "%d_%dmw" % [bx, bz])
FACADES[_rng.randi() % FACADES.size()], "%d_%dmw" % [bx, bz]) _kit_fill_side(c.x + BLOCK * 0.5, -1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, "%d_%dme" % [bx, bz])
_building(c.x + BLOCK * 0.5, -1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, 10.0, 2, # Cafe parasols scattered among the stalls
FACADES[_rng.randi() % FACADES.size()], "%d_%dme" % [bx, bz]) for pi in 3:
var pp := c + Vector3(_rng.randf_range(-9.0, 9.0), 0.1, _rng.randf_range(-12.0, 12.0))
_kit_prop(["detail-parasol-a", "detail-parasol-b"][pi % 2], pp, _rng.randf_range(0.0, 360.0), 6.0)
# Stall rows down the middle # Stall rows down the middle
for r in 2: for r in 2:
var x := c.x + (-5.0 if r == 0 else 5.0) var x := c.x + (-5.0 if r == 0 else 5.0)