## Manages rain and snow particle effects driven by the weather controller. @tool class_name WeatherVFX extends Node3D @export_group("Rain") @export var rain_amount: int = 4000 @export var rain_area: Vector3 = Vector3(80.0, 40.0, 80.0) @export var rain_speed: float = 20.0 @export var rain_color: Color = Color(0.7, 0.75, 0.85, 0.4) @export_group("Snow") @export var snow_amount: int = 2000 @export var snow_area: Vector3 = Vector3(80.0, 12.0, 80.0) @export var snow_speed: float = 4.0 @export var snow_color: Color = Color(0.95, 0.95, 1.0, 0.8) @export_group("Lightning") @export var lightning_color: Color = Color(0.9, 0.9, 1.0) @export var lightning_flash_duration: float = 0.15 @export_group("Rain Audio") @export var rain_audio_enabled: bool = true @export var rain_volume_db_max: float = 0.0 @export var rain_crossfade_threshold: float = 0.5 @export_group("Thunder") @export var thunder_enabled: bool = true @export var thunder_min_delay: float = 1.0 @export var thunder_max_delay: float = 5.0 @export var thunder_volume_db: float = -2.0 var _rain_particles: GPUParticles3D var _snow_particles: GPUParticles3D var _rain_material: ShaderMaterial var _snow_material: ShaderMaterial var _target_rain: float = 0.0 var _target_snow: float = 0.0 var _current_rain: float = 0.0 var _current_snow: float = 0.0 var _lightning_active: bool = false var _lightning_frequency: float = 0.0 var _lightning_timer: float = 0.0 var _lightning_next: float = 0.0 var _lightning_flash_timer: float = 0.0 var _lightning_light: DirectionalLight3D var _thunder_player: AudioStreamPlayer var _thunder_pending: bool = false var _thunder_delay_timer: float = 0.0 var _rain_light_player: AudioStreamPlayer var _rain_hard_player: AudioStreamPlayer func _ready() -> void: _create_rain_system() _create_snow_system() _create_lightning_light() _create_thunder_player() _create_rain_audio_players() func _process(delta: float) -> void: _current_rain = move_toward(_current_rain, _target_rain, delta * 0.5) _current_snow = move_toward(_current_snow, _target_snow, delta * 0.3) _rain_particles.emitting = _current_rain > 0.01 _rain_particles.amount = maxi(1, int(rain_amount * _current_rain)) if _rain_material: _rain_material.set_shader_parameter("alpha_mult", _current_rain) _snow_particles.emitting = _current_snow > 0.01 _snow_particles.amount = maxi(1, int(snow_amount * _current_snow)) if _snow_material: _snow_material.set_shader_parameter("alpha_mult", _current_snow) _process_lightning(delta) _process_thunder(delta) _process_rain_audio() _follow_camera() func _process_lightning(delta: float) -> void: if _lightning_flash_timer > 0.0: _lightning_flash_timer -= delta if _lightning_light: var flash := _lightning_flash_timer / lightning_flash_duration _lightning_light.light_energy = flash * 3.0 _lightning_light.visible = true if _lightning_flash_timer <= 0.0 and _lightning_light: _lightning_light.visible = false if not _lightning_active or _lightning_frequency <= 0.0: return _lightning_timer += delta if _lightning_timer >= _lightning_next: _lightning_timer = 0.0 _lightning_next = randf_range(1.0 / maxf(_lightning_frequency, 0.01) * 0.5, 1.0 / maxf(_lightning_frequency, 0.01) * 2.0) _trigger_lightning() func _trigger_lightning() -> void: _lightning_flash_timer = lightning_flash_duration if _lightning_light: _lightning_light.light_color = lightning_color _lightning_light.light_energy = 3.0 _lightning_light.visible = true _lightning_light.rotation_degrees.x = randf_range(-80.0, -50.0) _lightning_light.rotation_degrees.y = randf_range(-180.0, 180.0) if thunder_enabled: _thunder_pending = true _thunder_delay_timer = randf_range(thunder_min_delay, thunder_max_delay) func _process_thunder(delta: float) -> void: if not _thunder_pending: return _thunder_delay_timer -= delta if _thunder_delay_timer <= 0.0: _thunder_pending = false _play_thunder() func _play_thunder() -> void: if _thunder_player == null: return _thunder_player.play() func _create_thunder_player() -> void: _thunder_player = AudioStreamPlayer.new() _thunder_player.name = "ThunderPlayer" _thunder_player.volume_db = thunder_volume_db _thunder_player.bus = "Master" _thunder_player.stream = load("res://resources/thunder.mp3") add_child(_thunder_player) func _create_rain_audio_players() -> void: _rain_light_player = AudioStreamPlayer.new() _rain_light_player.name = "RainLightPlayer" _rain_light_player.volume_db = -80.0 _rain_light_player.bus = "Master" _rain_light_player.stream = load("res://resources/rain_light.mp3") add_child(_rain_light_player) _rain_hard_player = AudioStreamPlayer.new() _rain_hard_player.name = "RainHardPlayer" _rain_hard_player.volume_db = -80.0 _rain_hard_player.bus = "Master" _rain_hard_player.stream = load("res://resources/rain_gentle.mp3") add_child(_rain_hard_player) func _process_rain_audio() -> void: if not rain_audio_enabled: if _rain_light_player.playing: _rain_light_player.stop() if _rain_hard_player.playing: _rain_hard_player.stop() return if _current_rain < 0.01: if _rain_light_player.playing: _rain_light_player.stop() if _rain_hard_player.playing: _rain_hard_player.stop() return # Below threshold: only light rain plays, volume scales linearly 0 → max # Above threshold: light rain fades out, hard rain fades in if _current_rain <= rain_crossfade_threshold: # Light rain: volume from 0 to max based on percentage within the range var light_vol := _current_rain / rain_crossfade_threshold _set_rain_player(_rain_light_player, light_vol) _stop_rain_player(_rain_hard_player) else: # Crossfade zone: light fades out, hard fades in var t := (_current_rain - rain_crossfade_threshold) / (1.0 - rain_crossfade_threshold) _set_rain_player(_rain_light_player, 1.0 - t) _set_rain_player(_rain_hard_player, t) func _set_rain_player(player: AudioStreamPlayer, weight: float) -> void: if weight < 0.001: _stop_rain_player(player) return if not player.playing: player.volume_db = -80.0 player.play() player.volume_db = lerpf(-80.0, rain_volume_db_max, weight) func _stop_rain_player(player: AudioStreamPlayer) -> void: if player.playing: player.stop() func _create_lightning_light() -> void: _lightning_light = DirectionalLight3D.new() _lightning_light.name = "LightningFlash" _lightning_light.light_color = lightning_color _lightning_light.light_energy = 0.0 _lightning_light.shadow_enabled = false _lightning_light.visible = false add_child(_lightning_light) func _follow_camera() -> void: var vp := get_viewport() if vp == null: return var cam := vp.get_camera_3d() if cam: global_position = cam.global_position func set_rain_intensity(intensity: float) -> void: _target_rain = clampf(intensity, 0.0, 1.0) func set_snow_intensity(intensity: float) -> void: _target_snow = clampf(intensity, 0.0, 1.0) func set_lightning(active: bool, frequency: float = 0.1) -> void: _lightning_active = active _lightning_frequency = frequency if not active and _lightning_light: _lightning_light.visible = false func _create_rain_system() -> void: _rain_particles = GPUParticles3D.new() _rain_particles.name = "RainParticles" _rain_particles.amount = rain_amount _rain_particles.lifetime = rain_area.y / rain_speed _rain_particles.emitting = false _rain_particles.visibility_aabb = AABB(-rain_area / 2.0, rain_area) var mat := ParticleProcessMaterial.new() mat.direction = Vector3(0.0, -1.0, 0.0) mat.spread = 5.0 mat.initial_velocity_min = rain_speed * 0.8 mat.initial_velocity_max = rain_speed * 1.2 mat.gravity = Vector3(0.0, -9.8, 0.0) mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX mat.emission_box_extents = Vector3(rain_area.x / 2.0, 0.5, rain_area.z / 2.0) _rain_particles.process_material = mat _rain_particles.transform.origin.y = rain_area.y / 2.0 _rain_material = ShaderMaterial.new() _rain_material.shader = _get_rain_shader() _rain_material.set_shader_parameter("rain_color", rain_color) _rain_material.set_shader_parameter("alpha_mult", 0.0) var mesh := QuadMesh.new() mesh.size = Vector2(0.02, 0.6) mesh.material = _rain_material _rain_particles.draw_pass_1 = mesh add_child(_rain_particles) func _create_snow_system() -> void: _snow_particles = GPUParticles3D.new() _snow_particles.name = "SnowParticles" _snow_particles.amount = snow_amount _snow_particles.lifetime = snow_area.y / snow_speed * 2.0 _snow_particles.emitting = false _snow_particles.visibility_aabb = AABB(-snow_area / 2.0, snow_area) var mat := ParticleProcessMaterial.new() mat.direction = Vector3(0.0, -1.0, 0.0) mat.spread = 15.0 mat.initial_velocity_min = snow_speed * 0.5 mat.initial_velocity_max = snow_speed * 1.0 mat.gravity = Vector3(0.0, -3, 0.0) mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX mat.emission_box_extents = Vector3(snow_area.x / 2.0, 1.0, snow_area.z / 2.0) mat.turbulence_enabled = true mat.turbulence_noise_strength = 1.5 mat.turbulence_noise_speed_random = 0.5 mat.turbulence_noise_scale = 4.0 _snow_particles.process_material = mat _snow_particles.transform.origin.y = snow_area.y / 2.0 _snow_material = ShaderMaterial.new() _snow_material.shader = _get_snow_shader() _snow_material.set_shader_parameter("snow_color", snow_color) _snow_material.set_shader_parameter("alpha_mult", 0.0) var mesh := QuadMesh.new() mesh.size = Vector2(0.12, 0.12) mesh.material = _snow_material _snow_particles.draw_pass_1 = mesh add_child(_snow_particles) func _get_rain_shader() -> Shader: var shader := Shader.new() shader.code = """ shader_type spatial; render_mode unshaded, cull_disabled, depth_draw_opaque, blend_mix; uniform vec4 rain_color : source_color = vec4(0.7, 0.75, 0.85, 0.4); uniform float alpha_mult = 1.0; void vertex() { MODELVIEW_MATRIX = VIEW_MATRIX * mat4( vec4(1.0, 0.0, 0.0, 0.0), vec4(0.0, 1.0, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), MODEL_MATRIX[3] ); } void fragment() { float fade = smoothstep(0.0, 0.3, UV.y) * smoothstep(1.0, 0.7, UV.y); ALBEDO = rain_color.rgb; ALPHA = rain_color.a * fade * alpha_mult; } """ return shader func _get_snow_shader() -> Shader: var shader := Shader.new() shader.code = """ shader_type spatial; render_mode unshaded, cull_disabled, depth_draw_opaque, blend_mix; uniform vec4 snow_color : source_color = vec4(0.95, 0.95, 1.0, 0.8); uniform float alpha_mult = 1.0; void vertex() { MODELVIEW_MATRIX = VIEW_MATRIX * mat4( vec4(1.0, 0.0, 0.0, 0.0), vec4(0.0, 1.0, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), MODEL_MATRIX[3] ); } void fragment() { float dist = length(UV - vec2(0.5)); float circle = smoothstep(0.5, 0.3, dist); ALBEDO = snow_color.rgb; ALPHA = snow_color.a * circle * alpha_mult; } """ return shader