class_name EnvironmentManagerRoot extends Node3D const NOISE_TEXTURE: Texture2D = preload("res://core/daynight/noise.tres") const WEATHER_SHADER: Material = preload("res://core/daynight/weather_overlay.tres") const DYNAMIC_ENVIRONMENT_UPDATES_PER_FRAME: int = 32 #how many update of the environment (apply materials) will be done per frame const FORCE_WEATHER_MATERIAL_PATH_PARTS: PackedStringArray = [ "/chunk/material/field.tres", "/chunk/material/grassflat_chunk.tres", "/chunk/material/path_chunk.tres", "/chunk/material/railway_chunk.tres", "/chunk/material/terrain.tres", "/chunk/prop/house/house.tres", "/chunk/prop/house/house_emissiv.tres", ] @export var environment_config: EnvironmentConfig @export_group("Input") @export var block_keyboard_input: bool = false: set(value): if block_keyboard_input == value: return block_keyboard_input = value _apply_keyboard_input_block() @export_group("Camera") @export var camera: Camera3D @export var camera_pivot: Node3D @export_group("Particles") @export var particles_wind: GPUParticles3D @export var particles_snow: GPUParticles3D @export var particles_fireflies: GPUParticles3D @export var particles_rain: GPUParticles3D @export var godray: PackedScene @export var environment_dust: ColorRect @export var blur: ColorRect @export var environment_shadows: MeshInstance3D @export var fog: Node3D @export_group("Sound") @export var thunder_sounds: Array[AudioStream] @export var rain_sounds: Array[AudioStream] @export_group("Clouds") @export var cloud_material: ShaderMaterial #environment nodes @onready var sun: DirectionalLight3D = $"../DirectionalLight3D" @onready var environment: WorldEnvironment = $"../WorldEnvironment" var day_night_controller: DayNightController var weather_controller: WeatherController var day_tween: Tween var day_time: float = 0.0 var pending_environment_nodes: Dictionary = {} var weather_shader_no_noise: Material var _blocked_keyboard_events_by_action: Dictionary = {} static func is_keyboard_input_blocked(tree: SceneTree) -> bool: if tree == null: return false for node in tree.get_nodes_in_group("keyboard_input_blocker"): var environment_manager := node as EnvironmentManagerRoot if environment_manager != null and environment_manager.block_keyboard_input: return true return false func _enter_tree() -> void: add_to_group("keyboard_input_blocker") func _ready() -> void: _apply_keyboard_input_block() #connect shader when new node is added get_tree().node_added.connect(_on_tree_node_added) #set noise texture 2d to materials in special group ApplyWindNoiseToMaterials() #set weather overlay (snow + rain) to materials in special group ApplyWeatherShaderToMaterials() if day_night_controller == null: day_night_controller = DayNightController.new( environment_config ) day_night_controller.name = "DayNightController" day_night_controller.time_changed.connect(select_day_time) add_child(day_night_controller) if weather_controller == null: weather_controller = WeatherController.new( particles_wind, particles_snow, particles_fireflies, particles_rain, godray, environment_dust, blur, fog, environment_shadows, sun, environment, environment_config, camera, camera_pivot, thunder_sounds, rain_sounds ) weather_controller.name = "WeatherController" add_child(weather_controller) if day_night_controller != null: select_day_time(day_night_controller.current_time) func _process(_delta: float) -> void: _process_pending_environment_nodes() func _input(event: InputEvent) -> void: if block_keyboard_input and event is InputEventKey: get_viewport().set_input_as_handled() func _exit_tree() -> void: _restore_keyboard_action_events() remove_from_group("keyboard_input_blocker") func flush_pending_environment_nodes() -> void: _process_pending_environment_nodes(-1) func _apply_keyboard_input_block() -> void: if block_keyboard_input: _remove_keyboard_action_events() else: _restore_keyboard_action_events() func _remove_keyboard_action_events() -> void: if not _blocked_keyboard_events_by_action.is_empty(): return for action in InputMap.get_actions(): var keyboard_events: Array[InputEvent] = [] for event in InputMap.action_get_events(action): if event is InputEventKey: keyboard_events.append(event) if keyboard_events.is_empty(): continue _blocked_keyboard_events_by_action[action] = keyboard_events Input.action_release(action) for keyboard_event in keyboard_events: InputMap.action_erase_event(action, keyboard_event) func _restore_keyboard_action_events() -> void: for action in _blocked_keyboard_events_by_action.keys(): if not InputMap.has_action(action): continue for keyboard_event in _blocked_keyboard_events_by_action[action]: InputMap.action_add_event(action, keyboard_event) _blocked_keyboard_events_by_action.clear() func _on_tree_node_added(node: Node) -> void: if _should_queue_dynamic_environment_node(node): _queue_dynamic_environment_node(node) func _should_queue_dynamic_environment_node(node: Node) -> bool: if node.is_in_group("wind_node"): return true if node.is_in_group("weather_vegetables_node"): return true return node is Node3D func _queue_dynamic_environment_node(node: Node) -> void: if not is_instance_valid(node): return var ancestor = node.get_parent() while ancestor != null: if pending_environment_nodes.has(ancestor.get_instance_id()): return ancestor = ancestor.get_parent() var stale_ids: Array[int] = [] for node_id in pending_environment_nodes.keys(): var pending_node = pending_environment_nodes[node_id] if not is_instance_valid(pending_node): stale_ids.append(node_id) continue if node.is_ancestor_of(pending_node): stale_ids.append(node_id) for node_id in stale_ids: pending_environment_nodes.erase(node_id) pending_environment_nodes[node.get_instance_id()] = node func _process_pending_environment_nodes(max_nodes: int = DYNAMIC_ENVIRONMENT_UPDATES_PER_FRAME) -> void: var processed = 0 for node_id in pending_environment_nodes.keys(): if max_nodes >= 0 and processed >= max_nodes: break var node = pending_environment_nodes[node_id] pending_environment_nodes.erase(node_id) if not is_instance_valid(node): continue _apply_dynamic_environment_materials(node) processed += 1 func _apply_dynamic_environment_materials(node: Node) -> void: if not is_instance_valid(node): return if node.is_in_group("wind_node"): _apply_wind_noise_to_node(node, NOISE_TEXTURE) if node is Node3D: _apply_weather_overlay_to_node(node, _get_weather_overlay_material(node)) if _should_ignore_weather_overlay(node): _clear_weather_overlay_from_node(node) func ApplyWindNoiseToMaterials(): for node in get_tree().get_nodes_in_group("wind_node"): _apply_wind_noise_to_node(node, NOISE_TEXTURE) func _apply_wind_noise_to_node(node: Node, noise_tex: Texture2D) -> void: if node is GeometryInstance3D: var material_override := node.material_override as ShaderMaterial if material_override: material_override.set_shader_parameter("wind_noise", noise_tex) if node is MeshInstance3D: for surface_index in node.get_surface_override_material_count(): var surface_material := node.get_surface_override_material(surface_index) as ShaderMaterial if surface_material: surface_material.set_shader_parameter("wind_noise", noise_tex) for child in node.get_children(): _apply_wind_noise_to_node(child, noise_tex) func ApplyWeatherShaderToMaterials(): var current_scene := get_tree().current_scene if current_scene != null: _apply_weather_overlay_to_node(current_scene, _get_weather_overlay_material(current_scene)) func _get_weather_overlay_material(node: Node) -> Material: if not node.is_in_group("weather_overlay_no_noise"): return WEATHER_SHADER if weather_shader_no_noise == null: weather_shader_no_noise = WEATHER_SHADER.duplicate() as Material var shader_material := weather_shader_no_noise as ShaderMaterial if shader_material: shader_material.set_shader_parameter("snow_noise_enabled", false) return weather_shader_no_noise func _apply_weather_overlay_to_node(node: Node, material: Material) -> void: if _should_ignore_weather_overlay(node): _clear_weather_overlay_from_node(node) return if node is GeometryInstance3D: if _geometry_should_never_receive_weather_overlay(node): node.material_overlay = null elif _should_clear_weather_overlay(node) and not _geometry_forces_weather_overlay(node): node.material_overlay = null elif _geometry_uses_alpha_texture(node) or _geometry_should_skip_weather_overlay(node): node.material_overlay = null else: node.material_overlay = material var child_material := _get_weather_overlay_material(node) if child_material == WEATHER_SHADER and material == weather_shader_no_noise: child_material = material for child in node.get_children(): _apply_weather_overlay_to_node(child, child_material) func _clear_weather_overlay_from_node(node: Node) -> void: if node is GeometryInstance3D: node.material_overlay = null for child in node.get_children(): _clear_weather_overlay_from_node(child) func _geometry_uses_alpha_texture(node: GeometryInstance3D) -> bool: var material_override := node.material_override as ShaderMaterial if _shader_material_uses_alpha_texture(material_override): return true if node is MeshInstance3D: for surface_index in node.get_surface_override_material_count(): var surface_material := node.get_surface_override_material(surface_index) as ShaderMaterial if _shader_material_uses_alpha_texture(surface_material): return true if node.mesh: for surface_index in node.mesh.get_surface_count(): var mesh_material := node.mesh.surface_get_material(surface_index) as ShaderMaterial if _shader_material_uses_alpha_texture(mesh_material): return true return false func _geometry_should_never_receive_weather_overlay(node: GeometryInstance3D) -> bool: if node is GPUParticles3D: return true var node_name := String(node.name).to_lower() return node_name.contains("cloud") or node_name.contains("fog") or node_name.contains("godray") func _geometry_should_skip_weather_overlay(node: GeometryInstance3D) -> bool: if _geometry_forces_weather_overlay(node): return false if _node_name_indicates_water(node): return true var has_material: bool = false var all_materials_ignored: bool = true var material_override := node.material_override if material_override != null: has_material = true all_materials_ignored = all_materials_ignored and _material_ignores_weather_overlay(material_override) if node is MeshInstance3D: for surface_index in node.get_surface_override_material_count(): var surface_material: Material = node.get_surface_override_material(surface_index) if surface_material == null: continue has_material = true all_materials_ignored = all_materials_ignored and _material_ignores_weather_overlay(surface_material) if node.mesh: for surface_index in node.mesh.get_surface_count(): var mesh_material: Material = node.mesh.surface_get_material(surface_index) if mesh_material == null: continue has_material = true all_materials_ignored = all_materials_ignored and _material_ignores_weather_overlay(mesh_material) return has_material and all_materials_ignored func _geometry_forces_weather_overlay(node: GeometryInstance3D) -> bool: if _material_forces_weather_overlay(node.material_override): return true if node is MeshInstance3D: for surface_index in node.get_surface_override_material_count(): if _material_forces_weather_overlay(node.get_surface_override_material(surface_index)): return true if node.mesh: for surface_index in node.mesh.get_surface_count(): if _material_forces_weather_overlay(node.mesh.surface_get_material(surface_index)): return true return false func _material_forces_weather_overlay(material: Material) -> bool: if material == null: return false var resource_path := material.resource_path.to_lower() for path_part in FORCE_WEATHER_MATERIAL_PATH_PARTS: if resource_path.contains(path_part): return true return false func _node_name_indicates_water(node: Node) -> bool: var node_name := String(node.name).to_lower() return node_name.begins_with("water") or node_name.contains("/water") func _material_ignores_weather_overlay(material: Material) -> bool: if material == null: return false var resource_path := material.resource_path.to_lower() if resource_path.contains("/water") or resource_path.contains("water_"): return true if resource_path.contains("/cloud/") or resource_path.contains("cloud"): return true var base_material := material as BaseMaterial3D if base_material != null: return base_material.transparency != BaseMaterial3D.TRANSPARENCY_DISABLED or base_material.albedo_color.a < 0.99 return false func _shader_material_uses_alpha_texture(material: ShaderMaterial) -> bool: if material == null or material.shader == null: return false var shader_code := material.shader.code return shader_code.find("alpha_texture") != -1 or shader_code.find("ALPHA") != -1 or shader_code.find("opacity") != -1 func _should_ignore_weather_overlay(node: Node) -> bool: return node.is_in_group("weather_overlay_ignore") func _should_clear_weather_overlay(node: Node) -> bool: return node.is_in_group("weather_vegetables_node") func select_day_time(normalized_time: float) -> void: #set show_day_time_debug = true to show debug on screen #normalized_time is a value between 0 and 1; the time of the day is calculate as "normalized_time" * 1440; day_time is the step to pass from sunrise, to day, to sunset, to night #e.g. #Time normalized_time day_time #00:00 0.0000 3.0 #06:00 0.2500 1.0 #09:36 0.4000 1.75 #10:48 0.4500 2.0 #12:00 0.5000 2.0 #19:12 0.8000 2.0 #21:36 0.9000 2.5 #24:00 1.0000 3.0 if normalized_time < environment_config.sunrise: #Sunrise: night → morning day_time = 3.0 + (normalized_time / environment_config.sunrise) * 1.0 # 3.0 → 4.0 elif normalized_time < environment_config.day: #Morning: morning → afternoon var t: float = (normalized_time - environment_config.sunrise) / (environment_config.day - environment_config.sunrise) day_time = 1.0 + t # 1.0 → 2.0 elif normalized_time < environment_config.sunset: #Broad daylight day_time = 2.0 elif normalized_time < environment_config.night: #Sunset: afternoon → night var t: float = (normalized_time - environment_config.sunset) / (environment_config.night - environment_config.sunset) day_time = 2.0 + t # 2.0 → 3.0 else: #Night day_time = 3.0 if weather_controller: weather_controller.day_time = day_time _update_clouds_lighting(normalized_time) func _update_clouds_lighting(normalized_time: float) -> void: if not cloud_material: return if normalized_time < environment_config.sunrise: # NOTTE / PRE-ALBA (Colori scuri, nessun rim light solare) cloud_material.set_shader_parameter("cloud_color", Color("1a1c2e")) cloud_material.set_shader_parameter("shadow_color", Color("0d0e17")) cloud_material.set_shader_parameter("rim_color", Color("000000")) elif normalized_time < environment_config.day: # MATTINA / ALBA (Colori che iniziano a scaldarsi) cloud_material.set_shader_parameter("cloud_color", Color("fdf3e7")) cloud_material.set_shader_parameter("shadow_color", Color("5c537a")) cloud_material.set_shader_parameter("rim_color", Color("ff9e64")) elif normalized_time < environment_config.sunset: # PIENO GIORNO (Nuvole bianche brillanti, ombre azzurrate anime) cloud_material.set_shader_parameter("cloud_color", Color("ffffff")) cloud_material.set_shader_parameter("shadow_color", Color("687fa1")) cloud_material.set_shader_parameter("rim_color", Color("fff3cc")) elif normalized_time < environment_config.night: # TRAMONTO (Bordo dorato/arancione acceso e ombre violacee) cloud_material.set_shader_parameter("cloud_color", Color("fdf3e7")) cloud_material.set_shader_parameter("shadow_color", Color("5c537a")) cloud_material.set_shader_parameter("rim_color", Color("ff9e64")) else: # NOTTE FONDA (Ritorno al buio) cloud_material.set_shader_parameter("cloud_color", Color("1a1c2e")) cloud_material.set_shader_parameter("shadow_color", Color("0d0e17")) cloud_material.set_shader_parameter("rim_color", Color("000000"))