Files
tgcc/core/daynight/environment_manager.gd
2026-07-08 10:14:29 +02:00

485 lines
16 KiB
GDScript

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"))