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:
co-authored by
Claude Fable 5
parent
b0af2ea9ee
commit
67040ba12d
@@ -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.
@@ -42,8 +42,8 @@ const LAYOUT: Array[String] = [
|
||||
]
|
||||
|
||||
# Palette
|
||||
const ASPHALT := Color(0.3, 0.3, 0.37)
|
||||
const PAVING := Color(0.47, 0.45, 0.51)
|
||||
const ASPHALT := Color(0.38, 0.38, 0.45)
|
||||
const PAVING := Color(0.56, 0.54, 0.6)
|
||||
const STRIPE := Color(0.72, 0.72, 0.75)
|
||||
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),
|
||||
@@ -69,6 +69,117 @@ const SIGN_COLORS: Array[Color] = [
|
||||
var _spawn_points: Array[Vector3] = []
|
||||
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 ──────────────────────────────
|
||||
|
||||
@@ -106,13 +217,18 @@ func _deco_box(pos: Vector3, size: Vector3, color: Color, emissive: bool = false
|
||||
|
||||
func _build_environment() -> void:
|
||||
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_color = Color(0.58, 0.54, 0.66)
|
||||
env.environment.ambient_light_energy = 3.0
|
||||
env.environment.fog_density = 0.0011 # city haze for depth over 600m
|
||||
env.environment.ambient_light_energy = 1.7
|
||||
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")
|
||||
if sun:
|
||||
sun.light_energy = 3.0
|
||||
sun.light_energy = 2.2
|
||||
sun.rotation_degrees = Vector3(-38, 55, 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).
|
||||
func _standard_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
var half := BLOCK * 0.5
|
||||
var alley := _rng.randi() % 2 == 0
|
||||
var gap := 4.0 if alley else 0.0
|
||||
var depth := (BLOCK - gap) * 0.5
|
||||
for side in 2:
|
||||
var dirv := 1 if side == 0 else -1 # 0: faces west street, body +x
|
||||
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
|
||||
# Detailed kit buildings fill both street-facing edges
|
||||
_kit_fill_side(c.x - half, 1, c.z - half, c.z + half, "%d_%dW" % [bx, bz])
|
||||
_kit_fill_side(c.x + half, -1, c.z - half, c.z + half, "%d_%dE" % [bx, bz])
|
||||
# Fire escapes into the mid-block gap on some blocks
|
||||
if _rng.randi() % 2 == 0:
|
||||
for f in range(1, 3):
|
||||
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,
|
||||
@@ -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.
|
||||
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]
|
||||
# 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, 2.0, -(BLOCK - 4.0) * 0.5 + 0.1), Vector3(BLOCK - 8.0, 3.0, 0.25), GLASS, n + "_Glass", "glass")
|
||||
# Tower shaft
|
||||
var tw := BLOCK - 20.0
|
||||
_box_static(c + Vector3(0, FLOOR_H + (h - FLOOR_H) * 0.5, 0), Vector3(tw, h - FLOOR_H, tw), col, n, "concrete")
|
||||
# Window + sign bands on two faces
|
||||
for f in range(2, floors):
|
||||
var y := float(f) * FLOOR_H
|
||||
_deco_box(c + Vector3(-tw * 0.5 - 0.1, y + 1.7, 0), Vector3(0.14, 2.0, tw - 3.0), WINDOW)
|
||||
_deco_box(c + Vector3(tw * 0.5 + 0.1, y + 1.7, 0), Vector3(0.14, 2.0, tw - 3.0), WINDOW)
|
||||
# Mega vertical sign on the tower corner
|
||||
_box_static(c + Vector3(-tw * 0.5 - 0.5, FLOOR_H + (h - FLOOR_H) * 0.45, -tw * 0.5 + 1.5),
|
||||
Vector3(0.8, (h - FLOOR_H) * 0.8, 1.6), METAL, n + "_SignCol", "metal")
|
||||
for s2 in floors - 2:
|
||||
var sy := FLOOR_H * 2.0 + (h - FLOOR_H * 3.0) * float(s2) / float(maxi(floors - 3, 1))
|
||||
_deco_box(c + Vector3(-tw * 0.5 - 0.95, sy, -tw * 0.5 + 1.5), Vector3(0.14, 2.4, 1.4),
|
||||
SIGN_COLORS[s2 % SIGN_COLORS.size()], true)
|
||||
# Rooftop crown billboard both directions
|
||||
# Detailed kit skyscraper on the podium
|
||||
var keys := KIT_SKYSCRAPERS.keys()
|
||||
var key: String = keys[_rng.randi() % keys.size()]
|
||||
var dims: Vector3 = KIT_SKYSCRAPERS[key]
|
||||
var s := (BLOCK - 22.0) / dims.x # shaft footprint ~26m
|
||||
var h := dims.y * s + FLOOR_H
|
||||
var yaw := float(90 * (_rng.randi() % 4))
|
||||
var body := StaticBody3D.new()
|
||||
body.name = n
|
||||
body.set_meta("acoustic_material", "concrete")
|
||||
add_child(body)
|
||||
body.global_position = c + Vector3(0, FLOOR_H, 0)
|
||||
body.rotation_degrees.y = yaw
|
||||
var shape := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
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")
|
||||
_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)
|
||||
@@ -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.
|
||||
func _market_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
var n := "Mkt_%d_%d" % [bx, bz]
|
||||
# Back walls: modest 2-floor 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,
|
||||
FACADES[_rng.randi() % FACADES.size()], "%d_%dmw" % [bx, bz])
|
||||
_building(c.x + BLOCK * 0.5, -1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, 10.0, 2,
|
||||
FACADES[_rng.randi() % FACADES.size()], "%d_%dme" % [bx, bz])
|
||||
# Back walls: detailed kit buildings on east/west faces
|
||||
_kit_fill_side(c.x - BLOCK * 0.5, 1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, "%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])
|
||||
# Cafe parasols scattered among the stalls
|
||||
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
|
||||
for r in 2:
|
||||
var x := c.x + (-5.0 if r == 0 else 5.0)
|
||||
|
||||
Reference in New Issue
Block a user