Feat/visual audio overhaul #21

Merged
Dotts merged 31 commits from feat/visual-audio-overhaul into main 2026-07-21 14:09:18 -07:00
44 changed files with 188 additions and 46 deletions
Showing only changes of commit 67040ba12d - Show all commits
+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)