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
@@ -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