Integrate the Blender character, gear and prop library
NpcAvatar - select one of 8 library character variants by role/seed (with npc_base and the procedural rig as fallbacks), keep Rig/Head and the existing bone animation - tint only skin/cloth/pants/accessory/hair; fixed detail materials keep their colour, npc_vest keeps its hi-vis vest, firefighter/guard use dark uniform tints - attach caps, hoods, helmets, backpacks, SCBA tanks, lanyards and hand items at the documented head/back/chest/hand points (primitive fallbacks retained) WorldBuilder - prefer assets/models/blender/props/ for the six replacements and the new street dressing (street lamp/tree, palms, planters, A-frame, cone, barrier, stanchion, podium, handrail, wall lantern, double door, marquee, sedan) - fit the same colliders/navigation obstacles and keep the placement validator; collect library emissives (bulbs, marquee face, wall lanterns, door LED, sedan lights) for set_night_mode() - add a library triangle counter + 60k budget check (current ~27,986); toggle prop gallery to the library models; fix the marquee label placement Tests/smoke - character selection/gear attach/fixed-material tint tests; library prop load, emissive and triangle-budget tests; smoke_props routes a guest around a stanchion in open sidewalk (the old bench route is now crowded by trees) - headless 222/222, Python 68/68, all smokes pass, compare 11 good / 0 bad Docs: TODO Phase 3.5 moved to DONE, README/architecture/blender-assets/plan describe the integrated runtime, MEMORY records the integration rules.
This commit is contained in:
1 parent
d513e0b82c
commit
d8cfddbf89
23 files changed
+531
-165
No files matched your search
+154
-56
@@ -14,8 +14,14 @@ const T_SKYLINE := preload("res://assets/textures/skyline.png")
|
||||
|
||||
## Generated prop models, one `.glb` per street-furniture piece.
|
||||
const MODEL_DIR := "res://assets/models/generated/"
|
||||
const BLENDER_PROP_DIR := "res://assets/models/blender/props/"
|
||||
const MODEL_PROPS := ["bollard", "bench", "trash_bin", "planter_box",
|
||||
"newspaper_box", "fire_hydrant"]
|
||||
## Library props loaded in place of the generated set, plus new street dressing.
|
||||
const LIBRARY_PROPS := ["bollard", "bench", "trash_bin", "planter_box",
|
||||
"newspaper_box", "fire_hydrant", "street_lamp", "street_tree", "traffic_cone",
|
||||
"palm_tree", "round_planter", "a_frame_sign", "crowd_barrier", "stanchion",
|
||||
"security_podium"]
|
||||
|
||||
var root: Node3D = null
|
||||
## Door lock indicator mesh near the handle; toggled by GameManager.
|
||||
@@ -33,6 +39,11 @@ var _window_mats: Array[StandardMaterial3D] = []
|
||||
## not listed), so tests and the debug overlay can report real model usage.
|
||||
var loaded_models: Array[String] = []
|
||||
var fallback_props: Array[String] = []
|
||||
## Props that came from the Blender library rather than the generated set.
|
||||
var library_props: Array[String] = []
|
||||
var library_nodes: Array[Node3D] = []
|
||||
## Total triangle budget for the Blender library pieces placed in the world.
|
||||
const LIBRARY_TRIANGLE_BUDGET := 60000
|
||||
|
||||
func _init(root_node: Node3D) -> void:
|
||||
root = root_node
|
||||
@@ -48,6 +59,7 @@ func build() -> void:
|
||||
_build_crosswalk()
|
||||
_build_street_furniture()
|
||||
_build_street_detail()
|
||||
_build_library_dressing()
|
||||
_build_guard_spawn()
|
||||
_build_navmesh()
|
||||
set_night_mode(false)
|
||||
@@ -106,6 +118,16 @@ func _build_building() -> void:
|
||||
# Door lock indicator (green = open, red = locked), placed beside the handle
|
||||
door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, -3.82), Color(0.1, 0.9, 0.3))
|
||||
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
|
||||
# Blender glazed double door in front of the recessed frame.
|
||||
var door_model := _load_prop_scene("double_door")
|
||||
if door_model != null:
|
||||
door_model.name = "LibraryDoubleDoor"
|
||||
door_model.position = Vector3(0, 0.0, -3.84)
|
||||
building.add_child(door_model)
|
||||
library_nodes.append(door_model)
|
||||
for material in _emissive_materials(door_model):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
_build_facade_windows(building)
|
||||
var col := BoxShape3D.new()
|
||||
col.size = Vector3(40, 18, 4)
|
||||
@@ -257,21 +279,32 @@ func _build_skyline_backdrop() -> void:
|
||||
var _skyline_mat: StandardMaterial3D = null
|
||||
|
||||
func _build_marquee() -> void:
|
||||
## Striped awning above the door, then the neon sign strip + label.
|
||||
var awning := _add_box(root, Vector3(10.5, 0.08, 1.8), Vector3(0, 6.55, -3.7), Color(1, 1, 1), true, T_AWNING, Vector2(4, 1), "awning")
|
||||
awning.name = "Awning"
|
||||
## Neon-style marquee strip above the door: emissive bars + a label.
|
||||
var strip_parent := Node3D.new()
|
||||
strip_parent.name = "Marquee"
|
||||
for offset in [-2.2, 2.2]:
|
||||
var bar := _add_box(strip_parent, Vector3(0.25, 0.5, 0.5), Vector3(offset, 7.4, -3.6), Color(1, 0.35, 0.75))
|
||||
var mat := _emissive_material(Color(1.0, 0.35, 0.75))
|
||||
bar.material_override = mat
|
||||
_marquee_emissive.append(mat)
|
||||
## Blender canopy + sign box above the door; the procedural striped awning and
|
||||
## neon bars remain only as a fallback when the library asset is missing.
|
||||
var library := _load_prop_scene("marquee")
|
||||
if library != null:
|
||||
library.name = "LibraryMarquee"
|
||||
library.position = Vector3(0, 5.62, -3.9)
|
||||
root.add_child(library)
|
||||
library_nodes.append(library)
|
||||
for material in _emissive_materials(library):
|
||||
if not _marquee_emissive.has(material):
|
||||
_marquee_emissive.append(material)
|
||||
else:
|
||||
var awning := _add_box(root, Vector3(10.5, 0.08, 1.8), Vector3(0, 6.55, -3.7), Color(1, 1, 1), true, T_AWNING, Vector2(4, 1), "awning")
|
||||
awning.name = "Awning"
|
||||
var strip_parent := Node3D.new()
|
||||
strip_parent.name = "Marquee"
|
||||
for offset in [-2.2, 2.2]:
|
||||
var bar := _add_box(strip_parent, Vector3(0.25, 0.5, 0.5), Vector3(offset, 7.4, -3.6), Color(1, 0.35, 0.75))
|
||||
var mat := _emissive_material(Color(1.0, 0.35, 0.75))
|
||||
bar.material_override = mat
|
||||
_marquee_emissive.append(mat)
|
||||
root.add_child(strip_parent)
|
||||
marquee_label = Label3D.new()
|
||||
marquee_label.name = "MarqueeLabel"
|
||||
marquee_label.text = "GIG LINE"
|
||||
marquee_label.position = Vector3(0, 7.3, -3.55)
|
||||
marquee_label.position = Vector3(0, 6.45, -2.12)
|
||||
marquee_label.modulate = Color(1, 0.6, 0.9)
|
||||
marquee_label.font_size = 160
|
||||
marquee_label.pixel_size = 0.004
|
||||
@@ -279,61 +312,110 @@ func _build_marquee() -> void:
|
||||
marquee_label.outline_modulate = Color(0.9, 0.15, 0.5)
|
||||
marquee_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
marquee_label.no_depth_test = false
|
||||
strip_parent.add_child(marquee_label)
|
||||
root.add_child(strip_parent)
|
||||
root.add_child(marquee_label)
|
||||
|
||||
## Venue dressing that is not part of the sidewalk prop table: a stair handrail,
|
||||
## a parked sedan, and wall lanterns. Kept separate so the placement validator
|
||||
## only sees grounded sidewalk furniture.
|
||||
func _build_library_dressing() -> void:
|
||||
if ResourceLoader.exists(BLENDER_PROP_DIR + "handrail.glb"):
|
||||
var rail := _load_prop_scene("handrail")
|
||||
if rail != null:
|
||||
rail.name = "LibraryHandrail"
|
||||
rail.position = Vector3(3.35, 0.2, 3.95)
|
||||
rail.rotation_degrees.y = 90.0
|
||||
root.add_child(rail)
|
||||
library_nodes.append(rail)
|
||||
if ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"):
|
||||
var sedan := _add_prop_model(root, "sedan", Vector3(6.0, 0.1, 17.0), 180.0)
|
||||
sedan.name = "ParkedSedan"
|
||||
if not library_nodes.has(sedan):
|
||||
library_nodes.append(sedan)
|
||||
for material in _emissive_materials(sedan):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
if ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"):
|
||||
for side in [-4.9, 4.9]:
|
||||
var lantern := _load_prop_scene("wall_lantern")
|
||||
if lantern == null:
|
||||
continue
|
||||
lantern.name = "WallLantern"
|
||||
lantern.position = Vector3(side, 3.3, -3.78)
|
||||
root.add_child(lantern)
|
||||
library_nodes.append(lantern)
|
||||
for material in _emissive_materials(lantern):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
|
||||
## Emissive materials under a prop, used to tie library bulbs/LEDs to night mode.
|
||||
func _emissive_materials(holder: Node3D) -> Array[StandardMaterial3D]:
|
||||
var out: Array[StandardMaterial3D] = []
|
||||
for node in holder.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := node as MeshInstance3D
|
||||
if mi.mesh == null:
|
||||
continue
|
||||
for index in mi.mesh.get_surface_count():
|
||||
var standard := mi.mesh.surface_get_material(index) as StandardMaterial3D
|
||||
if standard != null and standard.emission_enabled \
|
||||
and standard.emission_energy_multiplier > 0.0:
|
||||
out.append(standard)
|
||||
return out
|
||||
|
||||
func _build_street_furniture() -> void:
|
||||
# Street lamps along the sidewalk (bulbs + omni lights, off by day).
|
||||
# Blender library street lamps (bulbs + omni lights, off by day). The lamp
|
||||
# arm points to local -X, so the left side is rotated 180 degrees to face
|
||||
# the sidewalk.
|
||||
for side in [-7.5, 7.5]:
|
||||
var lamp := Node3D.new()
|
||||
lamp.name = "Lamp_%d" % int(side)
|
||||
_add_box(lamp, Vector3(0.22, 5.0, 0.22), Vector3(side, 2.5, 8.5), Color(0.16, 0.16, 0.18), true, null, Vector2.ONE, "metal")
|
||||
_add_box(lamp, Vector3(1.6, 0.15, 0.15), Vector3(side + (0.8 if side < 0 else -0.8), 4.9, 8.5), Color(0.16, 0.16, 0.18), true, null, Vector2.ONE, "metal")
|
||||
var bulb := _add_box(lamp, Vector3(0.16, 0.16, 0.5), Vector3(side + (0.8 if side < 0 else -0.8), 4.85, 8.5), Color(1, 1, 0.85))
|
||||
var bulb_mat := _emissive_material(Color(1.0, 1.0, 0.8))
|
||||
bulb.material_override = bulb_mat
|
||||
bulb.material_override.emission_energy_multiplier = 1.5
|
||||
_lamp_bulb_mats.append(bulb_mat)
|
||||
var holder := _add_prop_model(root, "street_lamp",
|
||||
Vector3(side, 0.18, 8.5), 180.0 if side < 0.0 else 0.0)
|
||||
holder.name = "Lamp_%d" % int(side)
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(side + (0.8 if side < 0 else -0.8), 4.7, 8.5)
|
||||
omni.position = Vector3(-0.85, 4.65, 0)
|
||||
omni.omni_range = 12.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1, 0.93, 0.75)
|
||||
lamp.add_child(omni)
|
||||
holder.add_child(omni)
|
||||
_lamp_omni.append(omni)
|
||||
root.add_child(lamp)
|
||||
_street_lamps.append(bulb)
|
||||
# Planters beside the steps were flat boxes that clipped the treads; the
|
||||
# generated planter_box props replace them in _build_street_detail().
|
||||
# Low-poly trees along the street front.
|
||||
var trunk_color := Color(0.35, 0.25, 0.18)
|
||||
var leaf_color := Color(0.16, 0.42, 0.2)
|
||||
# Flank the walkway; keep the sight line to the door clear.
|
||||
_collect_emissive(holder)
|
||||
# Blender library street trees replace the cone trees. Flank the walkway and
|
||||
# keep the sight line to the door clear.
|
||||
for i in 6:
|
||||
var side := -1.0 if i % 2 == 0 else 1.0
|
||||
var x := side * (6.5 + float(int(i / 2)) * 3.0)
|
||||
var z := 4.0 + float((i * 7) % 5)
|
||||
var tree := Node3D.new()
|
||||
tree.name = "Tree_%d" % (i + 1)
|
||||
var trunk := MeshInstance3D.new()
|
||||
var trunk_mesh := CylinderMesh.new()
|
||||
trunk_mesh.top_radius = 0.09
|
||||
trunk_mesh.bottom_radius = 0.14
|
||||
trunk_mesh.height = 1.4
|
||||
trunk.mesh = trunk_mesh
|
||||
trunk.material_override = _plain_material(trunk_color, "wood")
|
||||
trunk.position = Vector3(x, 0.7, z)
|
||||
tree.add_child(trunk)
|
||||
var crown := MeshInstance3D.new()
|
||||
var crown_mesh := CylinderMesh.new()
|
||||
crown_mesh.top_radius = 0.05
|
||||
crown_mesh.bottom_radius = 0.85
|
||||
crown_mesh.height = 2.2
|
||||
crown.mesh = crown_mesh
|
||||
crown.material_override = _plain_material(leaf_color, "foliage")
|
||||
crown.position = Vector3(x, 2.1, z)
|
||||
tree.add_child(crown)
|
||||
root.add_child(tree)
|
||||
_add_prop_model(root, "street_tree", Vector3(x, 0.18, z),
|
||||
float((i * 37) % 360))
|
||||
|
||||
## Total triangles across the placed Blender library pieces, for the draw budget
|
||||
## test. Counts indices when present, otherwise the vertex stream.
|
||||
func library_triangle_count() -> int:
|
||||
var total := 0
|
||||
for holder in library_nodes:
|
||||
if not is_instance_valid(holder):
|
||||
continue
|
||||
for node in holder.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := node as MeshInstance3D
|
||||
if mi.mesh == null:
|
||||
continue
|
||||
for index in mi.mesh.get_surface_count():
|
||||
var arrays := mi.mesh.surface_get_arrays(index)
|
||||
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
||||
if indices.size() > 0:
|
||||
total += indices.size() / 3
|
||||
else:
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
total += vertices.size() / 3
|
||||
return total
|
||||
|
||||
## Register a library prop's emissive surfaces so night mode can switch them on.
|
||||
func _collect_emissive(holder: Node3D) -> void:
|
||||
for node in holder.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := node as MeshInstance3D
|
||||
if not _street_lamps.has(mi):
|
||||
_street_lamps.append(mi)
|
||||
for material in _emissive_materials(holder):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
|
||||
## Street-furniture placement table. Positions are in world space on the
|
||||
## sidewalk top (y = 0.18) and grouped by zone, so a cluster reads as street
|
||||
@@ -359,6 +441,17 @@ const PROP_PLACEMENTS := [
|
||||
"accessory": "crate"},
|
||||
{"zone": "curb", "prop": "fire_hydrant", "x": 8.4, "z": 11.2,
|
||||
"accessory": "cone"},
|
||||
# Blender library dressing: palms frame the block, planters and a queue
|
||||
# barrier sit outside the guest lane.
|
||||
{"zone": "mid_block", "prop": "palm_tree", "x": -13.0, "z": 10.2, "rot": 10.0},
|
||||
{"zone": "mid_block", "prop": "palm_tree", "x": 13.0, "z": 10.2, "rot": -10.0},
|
||||
{"zone": "mid_block", "prop": "round_planter", "x": -12.6, "z": 6.2},
|
||||
{"zone": "mid_block", "prop": "round_planter", "x": 12.6, "z": 6.2},
|
||||
{"zone": "curb", "prop": "a_frame_sign", "x": -6.9, "z": 6.5, "rot": 15.0},
|
||||
{"zone": "curb", "prop": "traffic_cone", "x": 9.6, "z": 9.8},
|
||||
{"zone": "curb", "prop": "crowd_barrier", "x": 3.0, "z": 10.8},
|
||||
{"zone": "curb", "prop": "stanchion", "x": -2.6, "z": 10.8},
|
||||
{"zone": "entrance", "prop": "security_podium", "x": 3.2, "z": 5.6, "rot": -15.0},
|
||||
]
|
||||
|
||||
## The door, steps, and the lane guests walk in through stay clear of props.
|
||||
@@ -449,6 +542,9 @@ func _add_prop_model(parent: Node3D, prop: String, pos: Vector3,
|
||||
if model != null:
|
||||
holder.add_child(model)
|
||||
loaded_models.append(prop)
|
||||
if ResourceLoader.exists(BLENDER_PROP_DIR + prop + ".glb"):
|
||||
library_props.append(prop)
|
||||
library_nodes.append(holder)
|
||||
else:
|
||||
var fallback := _fallback_prop(prop)
|
||||
holder.add_child(fallback)
|
||||
@@ -542,7 +638,9 @@ func _add_prop_accessory(holder: Node3D, kind: String) -> void:
|
||||
Color(0.92, 0.92, 0.9), "painted_metal")
|
||||
|
||||
func _load_prop_scene(prop: String) -> Node3D:
|
||||
var path := MODEL_DIR + prop + ".glb"
|
||||
var path := BLENDER_PROP_DIR + prop + ".glb"
|
||||
if not ResourceLoader.exists(path):
|
||||
path = MODEL_DIR + prop + ".glb"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
var res: Resource = ResourceLoader.load(path)
|
||||
|
||||
Reference in new issue
Block a user