From ed5e95280d259ca7b214dbe88b2f85de622b220d Mon Sep 17 00:00:00 2001 From: Kunal Siyag Date: Thu, 29 Oct 2026 00:20:04 +0530 Subject: [PATCH] fix(fp): range traffic signals and signs by view distance The traffic-light and stop/yield GLB instances spawn ~790 GeometryInstance3D children (signal heads, bodies) with no visibility range, so every junction in the day-10 city draws in street view regardless of distance. Range them at WS.VIS_PROP * vis_mult with self-fade, like the kit parts around them. Desktop census A/B (day-10 save, hero pose): FpMode in-range draws 955 -> 94, shadow casters 900 -> 39. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- docs/api/scripts.fp.traffic.md | 1 + scripts/fp/traffic/junctions.gd | 18 +++++++ scripts/fp/traffic/traffic_feature.gd | 1 + tools/tests/fp_junction_range_test.gd | 64 +++++++++++++++++++++++ tools/tests/fp_junction_range_test.gd.uid | 1 + 5 files changed, 85 insertions(+) create mode 100644 tools/tests/fp_junction_range_test.gd create mode 100644 tools/tests/fp_junction_range_test.gd.uid diff --git a/docs/api/scripts.fp.traffic.md b/docs/api/scripts.fp.traffic.md index a23b47c..5f80acb 100644 --- a/docs/api/scripts.fp.traffic.md +++ b/docs/api/scripts.fp.traffic.md @@ -486,6 +486,7 @@ Junction control (docs/fp/slices/E-traffic.md ยง2): 4-way traffic lights on cros ### Properties +- `vis_mult: float`: The quality preset's visibility scale (CityView.vis_mult), set by the traffic feature; the signal/sign GLBs are ranged with it at build time (street_dressing's _range convention). - `props: Dictionary`: tile -> {"node": Node3D, "groups": Array[MeshInstance3D] (axis * 3 + colour), "peds": Array[GeometryInstance3D] per (axis * 2 + walk), "poles": Array[Node3D], "state": int, "colliders": Array of holder-local solid entries (written into the view's holder meta)} - `holder_of: Callable`: The view's tile holder, so our posts can be written into its `colliders` meta (the channel CollisionWorld reads), and the collision refresh to call once they changed. traffic_feature wires both: only its ctx holds the view and the collision world. diff --git a/scripts/fp/traffic/junctions.gd b/scripts/fp/traffic/junctions.gd index c351753..eafc13e 100644 --- a/scripts/fp/traffic/junctions.gd +++ b/scripts/fp/traffic/junctions.gd @@ -43,6 +43,9 @@ var lg: LG var sim: CitySim var clock: float = 0.0 var root: Node3D +## The quality preset's visibility scale (CityView.vis_mult), set by the traffic feature; the +## signal/sign GLBs are ranged with it at build time (street_dressing's _range convention). +var vis_mult: float = 1.0 ## tile -> {"node": Node3D, "groups": Array[MeshInstance3D] (axis * 3 + colour), "peds": ## Array[GeometryInstance3D] per (axis * 2 + walk), "poles": Array[Node3D], "state": int, ## "colliders": Array of holder-local solid entries (written into the view's holder meta)} @@ -326,6 +329,7 @@ func _build_cross(i: int) -> Dictionary: var inst := _asset.instantiate() as Node3D if inst != null: pole.add_child(inst) + _range_asset(inst) _bind_asset_lamps(inst, axis, groups) _bind_asset_peds(inst, 1 - axis, ped_nodes) continue @@ -477,6 +481,19 @@ func _ped_signal(kit: KIT, at: Vector2, face: Vector2, hand: KIT, man: KIT) -> v ) +## The signal and sign GLBs arrive unranged: without this every lamp lens and sign face in the +## city drew at every distance (790 of 2732 draws on the day-10 draw census). Range them like the +## kit parts (_mi): VIS_PROP scaled by the quality preset, dithered out over half a metre. +func _range_asset(inst: Node3D) -> void: + for n in inst.find_children("*", "GeometryInstance3D", true, false): + var gi := n as GeometryInstance3D + if gi.visibility_range_end != 0.0: + continue + gi.visibility_range_end = WS.VIS_PROP * vis_mult + gi.visibility_range_end_margin = 0.5 + gi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF + + ## Binds H's traffic-light asset lamps (Signal_Red_*, Signal_Amber_*, Signal_Green_*) into the ## lamp groups: each node gets its own material_override on state changes. func _bind_asset_lamps(inst: Node, axis: int, groups: Array) -> void: @@ -518,6 +535,7 @@ func _build_tee(i: int, t: LG.TileInfo) -> Dictionary: var inst := ps.instantiate() as Node3D inst.transform = xf node.add_child(inst) + _range_asset(inst) else: kit.cyl(xf, 0.045 * M, 0.045 * M, 2.55 * M, POLE_COL, 8) var px := xf * Transform3D(Basis(), Vector3(0, 2.25 * M, 0.06 * M)) diff --git a/scripts/fp/traffic/traffic_feature.gd b/scripts/fp/traffic/traffic_feature.gd index d0d7a6b..fb9d7c7 100644 --- a/scripts/fp/traffic/traffic_feature.gd +++ b/scripts/fp/traffic/traffic_feature.gd @@ -271,6 +271,7 @@ func on_world_rebuilt() -> void: peds.rebuild_all() junctions.setup(lanes, _props_root) junctions.clock = clock + junctions.vis_mult = ctx.view.vis_mult if ctx.view != null else 1.0 # Junction posts are solid: junctions writes their colliders into the view's holder meta and # calls collision back; only this ctx holds the view and the collision world. junctions.holder_of = func(i: int) -> Node3D: diff --git a/tools/tests/fp_junction_range_test.gd b/tools/tests/fp_junction_range_test.gd new file mode 100644 index 0000000..728877f --- /dev/null +++ b/tools/tests/fp_junction_range_test.gd @@ -0,0 +1,64 @@ +extends SceneTree +## The traffic-light and sign GLBs must leave junctions._range_asset ranged: unranged, every +## lamp lens and sign face in the city drew at every distance (790 of 2732 draws on the day-10 +## draw census). The range follows the quality preset's vis_mult (the web tier's 0.55), and a +## node the asset pre-ranges is left alone. + +const JN := preload("res://scripts/fp/traffic/junctions.gd") +const AP := preload("res://scripts/fp/asset_paths.gd") +const WS := preload("res://scripts/fp/world_scale.gd") + +var _fail := 0 + + +func _check(ok: bool, name: String) -> void: + if ok: + print("PASS ", name) + else: + _fail += 1 + print("FAIL ", name) + + +func _initialize() -> void: + var jn: JN = JN.new() + jn.vis_mult = 0.55 + var ps := AP.load_scene(AP.TRAFFIC_LIGHT) + _check(ps != null, "traffic light asset loads") + var inst := (ps as PackedScene).instantiate() as Node3D + root.add_child(inst) + var gis := inst.find_children("*", "GeometryInstance3D", true, false) + _check(gis.size() > 0, "the asset has geometry") + (gis[0] as GeometryInstance3D).visibility_range_end = 99.0 + jn._range_asset(inst) + var want: float = WS.VIS_PROP * 0.55 + var unranged := 0 + var wrong := 0 + for g in gis: + var gi := g as GeometryInstance3D + if gi.visibility_range_end == 0.0: + unranged += 1 + elif ( + absf(gi.visibility_range_end - want) > 0.01 + and absf(gi.visibility_range_end - 99.0) > 0.01 + ): + wrong += 1 + _check(unranged == 0, "every part ranged (%d unranged)" % unranged) + _check(wrong == 0, "ranges follow the preset scale (%.2f, %d wrong)" % [want, wrong]) + _check( + absf((gis[0] as GeometryInstance3D).visibility_range_end - 99.0) < 0.01, + "a pre-ranged part keeps its own range" + ) + _check( + ( + (gis[1] as GeometryInstance3D).visibility_range_fade_mode + == GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF + ), + "parts fade out instead of popping" + ) + inst.free() + if _fail == 0: + print("FP_JUNCTION_RANGE_TEST: PASS") + quit(0) + else: + print("FP_JUNCTION_RANGE_TEST: FAIL (", _fail, ")") + quit(1) diff --git a/tools/tests/fp_junction_range_test.gd.uid b/tools/tests/fp_junction_range_test.gd.uid new file mode 100644 index 0000000..9f485e7 --- /dev/null +++ b/tools/tests/fp_junction_range_test.gd.uid @@ -0,0 +1 @@ +uid://bf0qd5pana6f6