extends Node const SHADER_INTERACTOR_LIMIT := 8 const CLEARING_LIMIT := 4 @export var grass_material: ShaderMaterial @export_range(1, SHADER_INTERACTOR_LIMIT, 1) var max_interactors := 8 @export_range(0.02, 0.5, 0.01) var update_interval := 0.08 @export var path_clearings: Array[NodePath] = [] @export var actor_root_path := NodePath("../../..") @export var foliage_root_path := NodePath("../..") @export var simulation_manager_path := NodePath("../../../SimulationManager") @export var primary_interactor_path := NodePath("../../../Player") var _elapsed := 0.0 var _trail_map := FoliageTrailMap.new() func _ready() -> void: # JajceWorld first isolates its mutable materials in the parent's _ready(). call_deferred("_configure_interaction") func _configure_interaction() -> void: _configure_path_clearings() var simulation := get_node_or_null(simulation_manager_path) if simulation != null and simulation.has_signal("state_restored"): simulation.connect("state_restored", reset_interaction) if is_processing(): _refresh_interactors(0.0) func set_interaction_enabled(enabled: bool) -> void: set_process(enabled) reset_interaction() _elapsed = update_interval func reset_interaction() -> void: _trail_map.clear() if grass_material != null: grass_material.set_shader_parameter("interactor_count", 0) _bind_trail_materials(false) func _configure_path_clearings() -> void: if grass_material == null: return var segments := PackedVector4Array() segments.resize(CLEARING_LIMIT) var widths := Vector4.ZERO var count := 0 for path in path_clearings.slice(0, CLEARING_LIMIT): var strip := get_node_or_null(path) as MeshInstance3D if strip == null or not strip.mesh is BoxMesh: continue var box := strip.mesh as BoxMesh var half_length := strip.global_basis.z * box.size.z * 0.5 var start := strip.global_position - half_length var end := strip.global_position + half_length segments[count] = Vector4(start.x, start.z, end.x, end.z) widths[count] = strip.global_basis.x.length() * box.size.x * 0.5 + 0.12 count += 1 grass_material.set_shader_parameter("clearing_count", count) grass_material.set_shader_parameter("clearing_segments", segments) grass_material.set_shader_parameter("clearing_half_widths", widths) func _process(delta: float) -> void: _elapsed += delta if _elapsed < update_interval: return _refresh_interactors(_elapsed) _elapsed = 0.0 func _refresh_interactors(delta: float = 0.08) -> void: if grass_material == null: return var camera := get_viewport().get_camera_3d() var origin := camera.global_position if camera != null else Vector3.ZERO var actor_root := get_node_or_null(actor_root_path) var primary := get_node_or_null(primary_interactor_path) var nearest: Array[Node3D] = [] var limit := clampi(max_interactors, 1, SHADER_INTERACTOR_LIMIT) for value in get_tree().get_nodes_in_group("grass_interactors"): var candidate := value as Node3D if ( candidate == null or candidate.is_queued_for_deletion() or not candidate.is_visible_in_tree() or candidate.get_viewport() != get_viewport() or (actor_root != null and not actor_root.is_ancestor_of(candidate)) ): continue var insert_at := nearest.size() var candidate_distance := origin.distance_squared_to(candidate.global_position) for index in nearest.size(): if ( candidate == primary or ( nearest[index] != primary and ( candidate_distance < origin.distance_squared_to(nearest[index].global_position) ) ) ): insert_at = index break nearest.insert(insert_at, candidate) if nearest.size() > limit: nearest.pop_back() var positions := PackedVector3Array() positions.resize(SHADER_INTERACTOR_LIMIT) for index in SHADER_INTERACTOR_LIMIT: positions[index] = Vector3(0.0, -1000.0, 0.0) var trail_positions: Dictionary[int, Vector3] = {} for index in nearest.size(): positions[index] = nearest[index].global_position trail_positions[nearest[index].get_instance_id()] = positions[index] grass_material.set_shader_parameter("interactor_count", nearest.size()) grass_material.set_shader_parameter("interactor_positions", positions) _trail_map.advance(delta, origin, trail_positions) _bind_trail_materials(true) func _bind_trail_materials(enabled: bool) -> void: if grass_material != null: grass_material.set_shader_parameter("foliage_trail_map", _trail_map.texture) grass_material.set_shader_parameter("foliage_trail_rect", _trail_map.world_rect) grass_material.set_shader_parameter("foliage_trails_enabled", enabled) var foliage_root := get_node_or_null(foliage_root_path) if foliage_root == null: return for value in get_tree().get_nodes_in_group("interactive_foliage"): var patch := value as StylizedBerryPatch if patch != null and foliage_root.is_ancestor_of(patch): patch.set_contact_field(_trail_map.texture, _trail_map.world_rect, enabled) if grass_material != null: patch.set_contact_actors( grass_material.get_shader_parameter("interactor_positions"), int(grass_material.get_shader_parameter("interactor_count")) if enabled else 0 )