Start dynamic sky

This commit is contained in:
2026-02-03 11:26:48 +01:00
parent dd1ba0fa9a
commit 5843e8a07a
49 changed files with 4448 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
@tool
class_name CloudSystem2D
extends Node3D
signal cloud_coverage_changed(coverage: float)
const MAX_LAYERS := 4
@export_group("Cloud Layers")
@export var layer_configs: Array[CloudLayerConfig] = []
@export var global_coverage: float = 0.5:
set(value):
global_coverage = clampf(value, 0.0, 1.0)
_update_coverage()
cloud_coverage_changed.emit(global_coverage)
@export_group("Evolution")
@export var evolution_enabled: bool = true
@export var evolution_speed: float = 0.01
@export var evolution_scale: float = 1.0
@export var evolution_seed: int = 0
@export_group("Wind Override")
@export var use_global_wind: bool = true
@export var global_wind_direction: Vector2 = Vector2(1.0, 0.0)
@export var global_wind_speed: float = 1.0
@export_group("Lighting")
@export var sun_direction: Vector3 = Vector3(0.0, 1.0, 0.0)
@export var sun_color: Color = Color.WHITE
@export var sun_intensity: float = 1.0
@export var moon_direction: Vector3 = Vector3(0.0, -1.0, 0.0)
@export var moon_color: Color = Color(0.7, 0.8, 1.0)
@export var moon_intensity: float = 0.3
@export_group("Performance")
@export var update_rate: float = 60.0
var _cloud_materials: Array[ShaderMaterial] = []
var _cloud_meshes: Array[MeshInstance3D] = []
var _time_accumulator: float = 0.0
var _evolution_offset: float = 0.0
var _update_timer: float = 0.0
func _ready() -> void:
if layer_configs.is_empty():
_initialize_default_layers()
_create_cloud_meshes()
_update_all_layers()
func _process(delta: float) -> void:
_update_timer += delta
var update_interval := 1.0 / update_rate
if _update_timer >= update_interval:
_update_timer = 0.0
_time_accumulator += delta * update_rate * update_interval
if evolution_enabled:
_evolution_offset += delta * evolution_speed
_update_flow(delta)
_update_lighting()
func _initialize_default_layers() -> void:
layer_configs.clear()
var low_layer := CloudLayerConfig.new()
low_layer.layer_type = CloudLayerConfig.LayerType.LOW
low_layer.opacity = 0.9
low_layer.flow_speed = 0.03
low_layer.parallax_depth = 0.3
low_layer.tiling = Vector2(2.0, 2.0)
layer_configs.append(low_layer)
var mid_layer := CloudLayerConfig.new()
mid_layer.layer_type = CloudLayerConfig.LayerType.MID
mid_layer.opacity = 0.7
mid_layer.flow_speed = 0.02
mid_layer.parallax_depth = 0.6
mid_layer.tiling = Vector2(3.0, 3.0)
layer_configs.append(mid_layer)
var high_layer := CloudLayerConfig.new()
high_layer.layer_type = CloudLayerConfig.LayerType.HIGH
high_layer.opacity = 0.5
high_layer.flow_speed = 0.01
high_layer.parallax_depth = 0.9
high_layer.tiling = Vector2(4.0, 4.0)
layer_configs.append(high_layer)
var wisps_layer := CloudLayerConfig.new()
wisps_layer.layer_type = CloudLayerConfig.LayerType.WISPS
wisps_layer.opacity = 0.3
wisps_layer.flow_speed = 0.015
wisps_layer.parallax_depth = 1.0
wisps_layer.tiling = Vector2(6.0, 6.0)
layer_configs.append(wisps_layer)
func _create_cloud_meshes() -> void:
for mesh in _cloud_meshes:
if is_instance_valid(mesh):
mesh.queue_free()
_cloud_meshes.clear()
_cloud_materials.clear()
for i in layer_configs.size():
var config := layer_configs[i]
if not config.enabled:
continue
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = "CloudLayer_%d" % i
var quad := QuadMesh.new()
quad.size = Vector2(1000.0, 1000.0)
quad.orientation = PlaneMesh.FACE_Y
mesh_instance.mesh = quad
var material := _create_cloud_material(config)
mesh_instance.material_override = material
var height := config.get_layer_height() * 100.0 + 50.0
mesh_instance.position.y = height
add_child(mesh_instance)
_cloud_meshes.append(mesh_instance)
_cloud_materials.append(material)
func _create_cloud_material(config: CloudLayerConfig) -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = _get_cloud_shader()
material.set_shader_parameter("tiling", config.tiling)
material.set_shader_parameter("opacity", config.opacity * global_coverage)
material.set_shader_parameter("color_tint", config.color_tint)
material.set_shader_parameter("shadow_color", config.shadow_color)
material.set_shader_parameter("flow_direction", config.flow_direction)
material.set_shader_parameter("flow_speed", config.flow_speed)
material.set_shader_parameter("light_influence", config.light_influence)
material.set_shader_parameter("rim_glow_intensity", config.rim_glow_intensity)
if config.texture:
material.set_shader_parameter("cloud_texture", config.texture)
return material
func _get_cloud_shader() -> Shader:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode unshaded, depth_draw_never, cull_disabled;
uniform sampler2D cloud_texture : source_color, filter_linear_mipmap, repeat_enable;
uniform vec2 tiling = vec2(4.0, 4.0);
uniform float opacity = 0.8;
uniform vec4 color_tint : source_color = vec4(1.0);
uniform vec4 shadow_color : source_color = vec4(0.6, 0.6, 0.7, 1.0);
uniform vec2 flow_direction = vec2(1.0, 0.0);
uniform float flow_speed = 0.02;
uniform float flow_offset = 0.0;
uniform vec3 light_direction = vec3(0.0, 1.0, 0.0);
uniform vec4 light_color : source_color = vec4(1.0);
uniform float light_intensity = 1.0;
uniform float light_influence = 1.0;
uniform float rim_glow_intensity = 0.3;
uniform float evolution_offset = 0.0;
uniform float coverage = 1.0;
varying vec2 world_uv;
void vertex() {
world_uv = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
}
void fragment() {
vec2 uv = world_uv * tiling * 0.001;
uv += flow_direction * flow_offset;
vec2 distorted_uv = uv + vec2(sin(uv.y * 10.0 + evolution_offset) * 0.01, cos(uv.x * 10.0 + evolution_offset) * 0.01);
vec4 cloud_sample = texture(cloud_texture, distorted_uv);
float cloud_alpha = cloud_sample.r * opacity * coverage;
float light_dot = dot(normalize(light_direction), vec3(0.0, 1.0, 0.0));
float light_factor = mix(0.5, 1.0, (light_dot + 1.0) * 0.5) * light_influence;
vec3 lit_color = mix(shadow_color.rgb, color_tint.rgb, light_factor);
lit_color *= light_color.rgb * light_intensity;
float rim = pow(1.0 - cloud_sample.r, 2.0) * rim_glow_intensity;
lit_color += light_color.rgb * rim;
ALBEDO = lit_color;
ALPHA = cloud_alpha;
}
"""
return shader
func _update_flow(delta: float) -> void:
for i in _cloud_materials.size():
if i >= layer_configs.size():
continue
var config := layer_configs[i]
var material := _cloud_materials[i]
var wind_dir := global_wind_direction if use_global_wind else config.flow_direction
var wind_spd := global_wind_speed if use_global_wind else 1.0
#var flow_offset: float = material.get_shader_parameter("flow_offset")
#flow_offset += config.flow_speed * wind_spd * delta
#
#material.set_shader_parameter("flow_offset", flow_offset)
#material.set_shader_parameter("flow_direction", wind_dir)
#smaterial.set_shader_parameter("evolution_offset", _evolution_offset)
func _update_lighting() -> void:
for material in _cloud_materials:
material.set_shader_parameter("light_direction", sun_direction)
material.set_shader_parameter("light_color", sun_color)
material.set_shader_parameter("light_intensity", sun_intensity)
func _update_coverage() -> void:
for i in _cloud_materials.size():
if i >= layer_configs.size():
continue
var config := layer_configs[i]
_cloud_materials[i].set_shader_parameter("coverage", global_coverage)
_cloud_materials[i].set_shader_parameter("opacity", config.opacity)
func _update_all_layers() -> void:
for i in _cloud_materials.size():
if i >= layer_configs.size():
continue
var config := layer_configs[i]
var material := _cloud_materials[i]
material.set_shader_parameter("tiling", config.tiling)
material.set_shader_parameter("opacity", config.opacity)
material.set_shader_parameter("color_tint", config.color_tint)
material.set_shader_parameter("shadow_color", config.shadow_color)
material.set_shader_parameter("flow_direction", config.flow_direction)
material.set_shader_parameter("flow_speed", config.flow_speed)
material.set_shader_parameter("light_influence", config.light_influence)
material.set_shader_parameter("rim_glow_intensity", config.rim_glow_intensity)
material.set_shader_parameter("coverage", global_coverage)
func set_layer_enabled(layer_index: int, enabled: bool) -> void:
if layer_index < 0 or layer_index >= _cloud_meshes.size():
return
_cloud_meshes[layer_index].visible = enabled
func set_layer_opacity(layer_index: int, opacity: float) -> void:
if layer_index < 0 or layer_index >= _cloud_materials.size():
return
if layer_index < layer_configs.size():
layer_configs[layer_index].opacity = opacity
_cloud_materials[layer_index].set_shader_parameter("opacity", opacity)
func set_layer_color(layer_index: int, color: Color) -> void:
if layer_index < 0 or layer_index >= _cloud_materials.size():
return
if layer_index < layer_configs.size():
layer_configs[layer_index].color_tint = color
_cloud_materials[layer_index].set_shader_parameter("color_tint", color)
func set_global_wind(direction: Vector2, speed: float) -> void:
global_wind_direction = direction.normalized()
global_wind_speed = speed
func set_sun_lighting(direction: Vector3, color: Color, intensity: float) -> void:
sun_direction = direction.normalized()
sun_color = color
sun_intensity = intensity
_update_lighting()
func set_moon_lighting(direction: Vector3, color: Color, intensity: float) -> void:
moon_direction = direction.normalized()
moon_color = color
moon_intensity = intensity
func rebuild_layers() -> void:
_create_cloud_meshes()
_update_all_layers()

View File

@@ -0,0 +1 @@
uid://cwplvsarljsf

View File

@@ -0,0 +1,313 @@
@tool
class_name DayNightController
extends Node
signal time_changed(time_of_day: float)
signal hour_changed(hour: int)
signal day_changed(day: int)
signal sunrise_started()
signal sunset_started()
signal night_started()
signal noon_reached()
const HOURS_PER_DAY := 24.0
const SUNRISE_START := 0.2 # ~5 AM
const SUNRISE_END := 0.3 # ~7 AM
const SUNSET_START := 0.75 # ~6 PM
const SUNSET_END := 0.85 # ~8 PM
const NOON := 0.5
@export_group("Time Settings")
@export var time_of_day: float = 0.25:
set(value):
var old_hour := get_current_hour()
time_of_day = wrapf(value, 0.0, 1.0)
var new_hour := get_current_hour()
if old_hour != new_hour:
hour_changed.emit(new_hour)
time_changed.emit(time_of_day)
_check_time_events(old_hour, new_hour)
@export var day_length_minutes: float = 30.0:
set(value):
day_length_minutes = clampf(value, 1.0, 1440.0)
@export var paused: bool = false
@export var time_scale: float = 1.0
@export var current_day: int = 1
@export_group("Sun Arc")
@export var sun_arc_tilt: float = 23.5
@export var sun_arc_offset: float = 0.0
@export_group("Moon Arc")
@export var moon_arc_tilt: float = 15.0
@export var moon_arc_offset: float = 180.0
@export var moon_phase: float = 0.0
@export_group("Sun Curves")
@export var sun_color_curve: Gradient
@export var sun_intensity_curve: Curve
@export_group("Moon Curves")
@export var moon_color_curve: Gradient
@export var moon_intensity_curve: Curve
@export_group("Ambient Curves")
@export var ambient_color_curve: Gradient
@export var ambient_intensity_curve: Curve
@export_group("Fog Curves")
@export var fog_color_curve: Gradient
@export var fog_density_curve: Curve
@export_group("Stars")
@export var stars_visibility_curve: Curve
var _last_hour: int = -1
var _sunrise_emitted := false
var _sunset_emitted := false
var _night_emitted := false
var _noon_emitted := false
func _ready() -> void:
_initialize_default_curves()
_last_hour = get_current_hour()
func _process(delta: float) -> void:
if paused:
return
var time_increment := delta / (day_length_minutes * 60.0) * time_scale
var old_time := time_of_day
time_of_day += time_increment
if time_of_day < old_time:
current_day += 1
day_changed.emit(current_day)
_reset_time_events()
func _initialize_default_curves() -> void:
if sun_color_curve == null:
sun_color_curve = Gradient.new()
sun_color_curve.set_color(0, Color(1.0, 0.6, 0.4)) # Dawn
sun_color_curve.add_point(0.25, Color(1.0, 0.95, 0.85)) # Morning
sun_color_curve.add_point(0.5, Color(1.0, 1.0, 0.95)) # Noon
sun_color_curve.add_point(0.75, Color(1.0, 0.7, 0.4)) # Sunset
sun_color_curve.set_color(1, Color(1.0, 0.5, 0.3)) # Dusk
if sun_intensity_curve == null:
sun_intensity_curve = Curve.new()
sun_intensity_curve.add_point(Vector2(0.0, 0.0))
sun_intensity_curve.add_point(Vector2(0.2, 0.0))
sun_intensity_curve.add_point(Vector2(0.3, 0.8))
sun_intensity_curve.add_point(Vector2(0.5, 1.0))
sun_intensity_curve.add_point(Vector2(0.7, 0.8))
sun_intensity_curve.add_point(Vector2(0.85, 0.0))
sun_intensity_curve.add_point(Vector2(1.0, 0.0))
if moon_color_curve == null:
moon_color_curve = Gradient.new()
moon_color_curve.set_color(0, Color(0.7, 0.8, 1.0))
moon_color_curve.set_color(1, Color(0.7, 0.8, 1.0))
if moon_intensity_curve == null:
moon_intensity_curve = Curve.new()
moon_intensity_curve.add_point(Vector2(0.0, 0.4))
moon_intensity_curve.add_point(Vector2(0.2, 0.3))
moon_intensity_curve.add_point(Vector2(0.25, 0.0))
moon_intensity_curve.add_point(Vector2(0.75, 0.0))
moon_intensity_curve.add_point(Vector2(0.85, 0.3))
moon_intensity_curve.add_point(Vector2(1.0, 0.4))
if ambient_color_curve == null:
ambient_color_curve = Gradient.new()
ambient_color_curve.set_color(0, Color(0.15, 0.18, 0.3)) # Night
ambient_color_curve.add_point(0.25, Color(0.5, 0.55, 0.65)) # Morning
ambient_color_curve.add_point(0.5, Color(0.6, 0.65, 0.75)) # Noon
ambient_color_curve.add_point(0.75, Color(0.55, 0.5, 0.55)) # Evening
ambient_color_curve.set_color(1, Color(0.15, 0.18, 0.3)) # Night
if ambient_intensity_curve == null:
ambient_intensity_curve = Curve.new()
ambient_intensity_curve.add_point(Vector2(0.0, 0.2))
ambient_intensity_curve.add_point(Vector2(0.25, 0.5))
ambient_intensity_curve.add_point(Vector2(0.5, 0.7))
ambient_intensity_curve.add_point(Vector2(0.75, 0.5))
ambient_intensity_curve.add_point(Vector2(1.0, 0.2))
if fog_color_curve == null:
fog_color_curve = Gradient.new()
fog_color_curve.set_color(0, Color(0.1, 0.12, 0.2))
fog_color_curve.add_point(0.25, Color(0.6, 0.65, 0.75))
fog_color_curve.add_point(0.5, Color(0.7, 0.75, 0.85))
fog_color_curve.add_point(0.75, Color(0.7, 0.55, 0.5))
fog_color_curve.set_color(1, Color(0.1, 0.12, 0.2))
if fog_density_curve == null:
fog_density_curve = Curve.new()
fog_density_curve.add_point(Vector2(0.0, 0.02))
fog_density_curve.add_point(Vector2(0.25, 0.015))
fog_density_curve.add_point(Vector2(0.5, 0.01))
fog_density_curve.add_point(Vector2(0.75, 0.015))
fog_density_curve.add_point(Vector2(1.0, 0.02))
if stars_visibility_curve == null:
stars_visibility_curve = Curve.new()
stars_visibility_curve.add_point(Vector2(0.0, 1.0))
stars_visibility_curve.add_point(Vector2(0.2, 0.8))
stars_visibility_curve.add_point(Vector2(0.3, 0.0))
stars_visibility_curve.add_point(Vector2(0.7, 0.0))
stars_visibility_curve.add_point(Vector2(0.85, 0.8))
stars_visibility_curve.add_point(Vector2(1.0, 1.0))
func _check_time_events(old_hour: int, new_hour: int) -> void:
if time_of_day >= SUNRISE_START and time_of_day < SUNRISE_END and not _sunrise_emitted:
_sunrise_emitted = true
sunrise_started.emit()
if time_of_day >= SUNSET_START and time_of_day < SUNSET_END and not _sunset_emitted:
_sunset_emitted = true
sunset_started.emit()
if time_of_day >= SUNSET_END and not _night_emitted:
_night_emitted = true
night_started.emit()
if time_of_day >= NOON - 0.01 and time_of_day < NOON + 0.01 and not _noon_emitted:
_noon_emitted = true
noon_reached.emit()
func _reset_time_events() -> void:
_sunrise_emitted = false
_sunset_emitted = false
_night_emitted = false
_noon_emitted = false
func get_current_hour() -> int:
return int(time_of_day * HOURS_PER_DAY)
func get_current_hour_float() -> float:
return time_of_day * HOURS_PER_DAY
func set_time_from_hours(hours: float) -> void:
time_of_day = fmod(hours, HOURS_PER_DAY) / HOURS_PER_DAY
func get_sun_direction() -> Vector3:
var angle := (time_of_day - 0.25) * TAU
var tilt_rad := deg_to_rad(sun_arc_tilt)
var offset_rad := deg_to_rad(sun_arc_offset)
var direction := Vector3(
cos(angle + offset_rad),
sin(angle),
sin(angle) * sin(tilt_rad)
).normalized()
return direction
func get_moon_direction() -> Vector3:
var angle := (time_of_day + 0.25) * TAU
var tilt_rad := deg_to_rad(moon_arc_tilt)
var offset_rad := deg_to_rad(moon_arc_offset)
var direction := Vector3(
cos(angle + offset_rad),
sin(angle),
sin(angle) * sin(tilt_rad)
).normalized()
return direction
func get_sun_color() -> Color:
if sun_color_curve:
return sun_color_curve.sample(time_of_day)
return Color.WHITE
func get_sun_intensity() -> float:
if sun_intensity_curve:
return sun_intensity_curve.sample(time_of_day)
return 1.0
func get_moon_color() -> Color:
if moon_color_curve:
return moon_color_curve.sample(time_of_day)
return Color(0.7, 0.8, 1.0)
func get_moon_intensity() -> float:
var base_intensity := 1.0
if moon_intensity_curve:
base_intensity = moon_intensity_curve.sample(time_of_day)
var phase_factor := 0.5 + 0.5 * cos(moon_phase * TAU)
return base_intensity * phase_factor
func get_ambient_color() -> Color:
if ambient_color_curve:
return ambient_color_curve.sample(time_of_day)
return Color(0.5, 0.55, 0.65)
func get_ambient_intensity() -> float:
if ambient_intensity_curve:
return ambient_intensity_curve.sample(time_of_day)
return 0.5
func get_fog_color() -> Color:
if fog_color_curve:
return fog_color_curve.sample(time_of_day)
return Color(0.7, 0.75, 0.85)
func get_fog_density() -> float:
if fog_density_curve:
return fog_density_curve.sample(time_of_day)
return 0.01
func get_stars_visibility() -> float:
if stars_visibility_curve:
return stars_visibility_curve.sample(time_of_day)
return 0.0
func is_daytime() -> bool:
return time_of_day >= SUNRISE_END and time_of_day < SUNSET_START
func is_nighttime() -> bool:
return time_of_day < SUNRISE_START or time_of_day >= SUNSET_END
func is_dawn() -> bool:
return time_of_day >= SUNRISE_START and time_of_day < SUNRISE_END
func is_dusk() -> bool:
return time_of_day >= SUNSET_START and time_of_day < SUNSET_END
func get_time_period() -> String:
if is_dawn():
return "dawn"
elif is_dusk():
return "dusk"
elif is_daytime():
return "day"
else:
return "night"

View File

@@ -0,0 +1 @@
uid://gypnoh5ikwjn

View File

@@ -0,0 +1,399 @@
@tool
class_name DynamicSkyRoot
extends Node3D
signal time_of_day_changed(time: float)
signal weather_changed(state: WeatherProfile.WeatherState)
signal preset_changed(preset: SkyPreset)
@export_group("Controllers")
@export var preset_library: SkyboxPresetLibrary
@export var day_night_controller: DayNightController
@export var weather_controller: WeatherController
@export var cloud_system: CloudSystem2D
@export var post_process_controller: PostProcessController
@export var vfx_controller: VFXController
@export var shadow_proxy_system: ShadowProxySystem
@export_group("Environment")
@export var world_environment: WorldEnvironment
@export var sun_light: DirectionalLight3D
@export var moon_light: DirectionalLight3D
@export_group("Global Settings")
@export var auto_create_controllers: bool = true
@export var deterministic_seed: int = 0:
set(value):
deterministic_seed = value
_apply_seed()
@export_group("Debug")
@export var show_debug_panel: bool = false
@export var debug_time_override: float = -1.0
var _current_config: SkyConfig
var preset_name: String = ""
var _updating_preset_name: bool = false
func _ready() -> void:
if auto_create_controllers:
_ensure_controllers_exist()
_connect_signals()
property_list_changed_notify()
_sync_preset_name()
_apply_seed()
_sync_all_systems()
func _process(delta: float) -> void:
_sync_all_systems()
if show_debug_panel:
_update_debug_info()
func _ensure_controllers_exist() -> void:
if preset_library == null:
preset_library = SkyboxPresetLibrary.new()
preset_library.name = "PresetLibrary"
add_child(preset_library)
if day_night_controller == null:
day_night_controller = DayNightController.new()
day_night_controller.name = "DayNightController"
add_child(day_night_controller)
if weather_controller == null:
weather_controller = WeatherController.new()
weather_controller.name = "WeatherController"
add_child(weather_controller)
if cloud_system == null:
cloud_system = CloudSystem2D.new()
cloud_system.name = "CloudSystem"
add_child(cloud_system)
if post_process_controller == null:
post_process_controller = PostProcessController.new()
post_process_controller.name = "PostProcessController"
add_child(post_process_controller)
if vfx_controller == null:
vfx_controller = VFXController.new()
vfx_controller.name = "VFXController"
add_child(vfx_controller)
if shadow_proxy_system == null:
shadow_proxy_system = ShadowProxySystem.new()
shadow_proxy_system.name = "ShadowProxySystem"
add_child(shadow_proxy_system)
if world_environment == null:
_create_default_environment()
if sun_light == null:
_create_sun_light()
if moon_light == null:
_create_moon_light()
func _create_default_environment() -> void:
world_environment = WorldEnvironment.new()
world_environment.name = "WorldEnvironment"
world_environment.environment = Environment.new()
world_environment.environment.background_mode = Environment.BG_SKY
world_environment.environment.sky = Sky.new()
add_child(world_environment)
if post_process_controller:
post_process_controller.target_environment = world_environment.environment
func _create_sun_light() -> void:
sun_light = DirectionalLight3D.new()
sun_light.name = "SunLight"
sun_light.light_color = Color(1.0, 0.95, 0.85)
sun_light.light_energy = 1.0
sun_light.shadow_enabled = true
add_child(sun_light)
func _create_moon_light() -> void:
moon_light = DirectionalLight3D.new()
moon_light.name = "MoonLight"
moon_light.light_color = Color(0.7, 0.8, 1.0)
moon_light.light_energy = 0.3
moon_light.shadow_enabled = false
add_child(moon_light)
func _connect_signals() -> void:
if day_night_controller:
day_night_controller.time_changed.connect(_on_time_changed)
if weather_controller:
weather_controller.weather_state_changed.connect(_on_weather_state_changed)
if preset_library:
preset_library.preset_changed.connect(_on_preset_changed)
func _on_time_changed(time: float) -> void:
time_of_day_changed.emit(time)
if vfx_controller:
vfx_controller.set_night_mode(day_night_controller.is_nighttime())
func _on_weather_state_changed(old_state: WeatherProfile.WeatherState, new_state: WeatherProfile.WeatherState) -> void:
weather_changed.emit(new_state)
func _on_preset_changed(preset: SkyPreset) -> void:
preset_changed.emit(preset)
if preset.config:
_current_config = preset.config
_sync_preset_name()
_apply_preset_assets(preset)
func _apply_seed() -> void:
if deterministic_seed != 0:
seed(deterministic_seed)
func _sync_all_systems() -> void:
if day_night_controller == null:
return
var time := day_night_controller.time_of_day
if debug_time_override >= 0:
time = debug_time_override
_sync_sun_moon()
_sync_clouds()
_sync_post_process()
_sync_shadows()
func _sync_sun_moon() -> void:
if day_night_controller == null:
return
var sun_dir := day_night_controller.get_sun_direction()
var moon_dir := day_night_controller.get_moon_direction()
if sun_light:
sun_light.look_at(global_position - sun_dir, Vector3.UP)
sun_light.light_color = day_night_controller.get_sun_color()
sun_light.light_energy = day_night_controller.get_sun_intensity()
sun_light.visible = day_night_controller.get_sun_intensity() > 0.01
if moon_light:
moon_light.look_at(global_position - moon_dir, Vector3.UP)
moon_light.light_color = day_night_controller.get_moon_color()
moon_light.light_energy = day_night_controller.get_moon_intensity()
moon_light.visible = day_night_controller.get_moon_intensity() > 0.01
if world_environment and world_environment.environment:
world_environment.environment.ambient_light_color = day_night_controller.get_ambient_color()
world_environment.environment.ambient_light_energy = day_night_controller.get_ambient_intensity()
func _sync_clouds() -> void:
if cloud_system == null:
return
if day_night_controller:
cloud_system.set_sun_lighting(
day_night_controller.get_sun_direction(),
day_night_controller.get_sun_color(),
day_night_controller.get_sun_intensity()
)
cloud_system.set_moon_lighting(
day_night_controller.get_moon_direction(),
day_night_controller.get_moon_color(),
day_night_controller.get_moon_intensity()
)
if weather_controller:
cloud_system.global_coverage = weather_controller.get_cloud_coverage()
cloud_system.set_global_wind(
weather_controller.get_wind_direction(),
weather_controller.get_wind_speed()
)
func _sync_post_process() -> void:
if post_process_controller == null or day_night_controller == null:
return
var sun_dir := day_night_controller.get_sun_direction()
var sun_color := day_night_controller.get_sun_color()
post_process_controller.set_sun_glow(
sun_dir,
sun_color,
day_night_controller.get_sun_intensity() * 0.5,
0.2
)
post_process_controller.set_fog_settings(
day_night_controller.get_fog_color(),
day_night_controller.get_fog_density()
)
var dawn_dusk_blend := 0.0
if day_night_controller.is_dawn() or day_night_controller.is_dusk():
dawn_dusk_blend = 1.0
post_process_controller.set_dawn_dusk_blend(dawn_dusk_blend)
post_process_controller.set_stars_visibility(
day_night_controller.get_stars_visibility()
)
if _current_config:
post_process_controller.set_sky_colors(
_current_config.sky_top_color,
_current_config.sky_horizon_color,
_current_config.sky_bottom_color
)
post_process_controller.gradient_intensity = _current_config.gradient_intensity
post_process_controller.apply_config(_current_config)
func _apply_preset_assets(preset: SkyPreset) -> void:
if preset == null:
return
if post_process_controller:
post_process_controller.skybox_texture = preset.skybox_texture
if preset.post_process_profile:
post_process_controller.profile = preset.post_process_profile
if cloud_system and preset.cloud_layer_configs.size() > 0:
cloud_system.layer_configs = preset.cloud_layer_configs
cloud_system.rebuild_layers()
func _get_property_list() -> Array:
var properties := []
var names: PackedStringArray = []
if preset_library:
names = preset_library.get_preset_names()
var hint_string := ""
if names.size() > 0:
hint_string = ",".join(names)
properties.append({
"name": "preset_name",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_ENUM,
"hint_string": hint_string,
})
return properties
func _set(property: StringName, value: Variant) -> bool:
if property == "preset_name":
if preset_name == value:
return true
preset_name = value
if _updating_preset_name:
return true
if preset_library and String(preset_name) != "":
preset_library.apply_preset(String(preset_name))
return true
return false
func _get(property: StringName) -> Variant:
if property == "preset_name":
return preset_name
return null
func _sync_preset_name() -> void:
if preset_library == null:
return
var current := preset_library.get_current_preset()
if current == null:
return
_updating_preset_name = true
preset_name = current.preset_name
_updating_preset_name = false
func _sync_shadows() -> void:
if shadow_proxy_system == null:
return
if weather_controller:
shadow_proxy_system.set_cloud_coverage(weather_controller.get_cloud_coverage())
if cloud_system:
shadow_proxy_system.sync_with_cloud_system(cloud_system)
func _update_debug_info() -> void:
pass
func set_time_of_day(time: float) -> void:
if day_night_controller:
day_night_controller.time_of_day = time
func set_weather(state: WeatherProfile.WeatherState, transition_time: float = -1.0) -> void:
if weather_controller:
weather_controller.set_weather(state, transition_time)
func apply_preset(preset_name: String) -> void:
if preset_library:
preset_library.apply_preset(preset_name)
func pause_time() -> void:
if day_night_controller:
day_night_controller.paused = true
func resume_time() -> void:
if day_night_controller:
day_night_controller.paused = false
func trigger_meteor_shower(duration: float = 60.0) -> void:
if vfx_controller:
vfx_controller.start_meteor_shower(duration)
func get_time_of_day() -> float:
if day_night_controller:
return day_night_controller.time_of_day
return 0.0
func get_current_weather() -> WeatherProfile.WeatherState:
if weather_controller:
return weather_controller.current_weather
return WeatherProfile.WeatherState.CLEAR
func is_daytime() -> bool:
if day_night_controller:
return day_night_controller.is_daytime()
return true
func is_nighttime() -> bool:
if day_night_controller:
return day_night_controller.is_nighttime()
return false

View File

@@ -0,0 +1 @@
uid://8r3do6ow5nmn

View File

@@ -0,0 +1,310 @@
@tool
class_name PostProcessController
extends Node
@export_group("Profile")
@export var profile: PostProcessProfile:
set(value):
profile = value
_apply_profile()
@export_group("Environment")
@export var target_environment: Environment
@export_group("Sky Gradient")
@export var sky_top_color: Color = Color(0.2, 0.4, 0.8)
@export var sky_horizon_color: Color = Color(0.6, 0.7, 0.9)
@export var sky_bottom_color: Color = Color(0.4, 0.5, 0.6)
@export var gradient_intensity: float = 1.0
@export_group("Skybox")
@export var skybox_texture: Texture2D:
set(value):
skybox_texture = value
_update_sky_material()
@export var skybox_blend: float = 1.0:
set(value):
skybox_blend = clampf(value, 0.0, 1.0)
_update_sky_material()
@export_group("Fog Shaping")
@export var fog_enabled: bool = true
@export var fog_color: Color = Color(0.7, 0.8, 0.9)
@export var fog_density: float = 0.01
@export var height_fog_enabled: bool = true
@export var height_fog_min: float = 0.0
@export var height_fog_max: float = 100.0
@export var height_fog_curve: float = 1.0
@export_group("Sun Glow")
@export var sun_glow_enabled: bool = true
@export var sun_glow_intensity: float = 0.5
@export var sun_glow_size: float = 0.2
@export var sun_glow_falloff: float = 2.0
@export var sun_glow_color: Color = Color(1.0, 0.95, 0.8)
@export var sun_direction: Vector3 = Vector3(0.0, 1.0, 0.0)
@export_group("Dawn/Dusk Enhancement")
@export var dawn_dusk_enabled: bool = true
@export var dawn_dusk_intensity: float = 0.5
@export var dawn_dusk_blend: float = 0.0
@export_group("Performance")
@export var effects_enabled: bool = true
var _sky_material: ShaderMaterial
var _post_process_quad: MeshInstance3D
func _ready() -> void:
if profile == null:
profile = PostProcessProfile.new()
_apply_profile()
_setup_environment()
func _apply_profile() -> void:
if profile == null:
return
gradient_intensity = profile.gradient_intensity
fog_enabled = profile.fog_enabled
height_fog_enabled = profile.height_fog_enabled
height_fog_min = profile.height_fog_start
height_fog_max = profile.height_fog_end
height_fog_curve = profile.height_fog_curve
sun_glow_enabled = profile.sun_glow_enabled
sun_glow_intensity = profile.sun_glow_intensity
sun_glow_size = profile.sun_glow_size
sun_glow_falloff = profile.sun_glow_falloff
sun_glow_color = profile.sun_glow_color
dawn_dusk_enabled = profile.dawn_dusk_enabled
dawn_dusk_intensity = profile.dawn_dusk_intensity
func _setup_environment() -> void:
if target_environment == null:
return
_update_fog()
_update_sky()
func _update_fog() -> void:
if target_environment == null:
return
target_environment.fog_enabled = fog_enabled and effects_enabled
if fog_enabled:
target_environment.fog_light_color = fog_color
target_environment.fog_density = fog_density
if height_fog_enabled:
target_environment.fog_height = height_fog_min
target_environment.fog_height_density = height_fog_curve * 0.1
func _update_sky() -> void:
if target_environment == null:
return
if target_environment.sky == null:
target_environment.sky = Sky.new()
if target_environment.sky.sky_material == null or not target_environment.sky.sky_material is ShaderMaterial:
_create_sky_material()
_sky_material = target_environment.sky.sky_material as ShaderMaterial
if _sky_material:
_update_sky_material()
func _create_sky_material() -> void:
_sky_material = ShaderMaterial.new()
_sky_material.shader = _get_sky_shader()
if target_environment and target_environment.sky:
target_environment.sky.sky_material = _sky_material
func _get_sky_shader() -> Shader:
var shader := Shader.new()
shader.code = """
shader_type sky;
uniform sampler2D skybox_texture : source_color, filter_linear_mipmap, repeat_enable;
uniform bool skybox_enabled = false;
uniform float skybox_blend = 1.0;
uniform vec4 sky_top_color : source_color = vec4(0.2, 0.4, 0.8, 1.0);
uniform vec4 sky_horizon_color : source_color = vec4(0.6, 0.7, 0.9, 1.0);
uniform vec4 sky_bottom_color : source_color = vec4(0.4, 0.5, 0.6, 1.0);
uniform float gradient_intensity = 1.0;
uniform float horizon_blend = 0.5;
uniform bool sun_glow_enabled = true;
uniform vec3 sun_direction = vec3(0.0, 1.0, 0.0);
uniform vec4 sun_glow_color : source_color = vec4(1.0, 0.95, 0.8, 1.0);
uniform float sun_glow_intensity = 0.5;
uniform float sun_glow_size = 0.2;
uniform float sun_glow_falloff = 2.0;
uniform bool dawn_dusk_enabled = true;
uniform vec4 dawn_dusk_color : source_color = vec4(1.0, 0.6, 0.4, 1.0);
uniform float dawn_dusk_intensity = 0.5;
uniform float dawn_dusk_blend = 0.0;
uniform float stars_visibility = 0.0;
uniform vec4 stars_tint : source_color = vec4(1.0);
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float stars(vec3 dir) {
vec2 uv = dir.xz / (abs(dir.y) + 0.001) * 50.0;
vec2 cell = floor(uv);
vec2 local = fract(uv) - 0.5;
float star = 0.0;
float h = hash(cell);
if (h > 0.97) {
float dist = length(local);
star = smoothstep(0.1, 0.0, dist) * (h - 0.97) / 0.03;
star *= step(0.0, dir.y);
}
return star;
}
vec2 pano_uv(vec3 dir) {
dir = normalize(dir);
float u = atan(dir.x, dir.z) / (2.0 * PI) + 0.5;
float v = acos(clamp(dir.y, -1.0, 1.0)) / PI;
return vec2(u, v);
}
void sky() {
vec3 dir = EYEDIR;
float y = dir.y;
vec3 color;
if (y > 0.0) {
float t = pow(y, horizon_blend);
color = mix(sky_horizon_color.rgb, sky_top_color.rgb, t);
} else {
float t = pow(-y, horizon_blend);
color = mix(sky_horizon_color.rgb, sky_bottom_color.rgb, t);
}
color = mix(vec3(0.5), color, gradient_intensity);
if (skybox_enabled) {
vec3 pano_color = texture(skybox_texture, pano_uv(dir)).rgb;
color = mix(color, pano_color, skybox_blend);
}
if (sun_glow_enabled && sun_direction.y > -0.2) {
float sun_dot = max(0.0, dot(dir, normalize(sun_direction)));
float glow = pow(sun_dot, 1.0 / max(0.001, sun_glow_size));
glow = pow(glow, sun_glow_falloff);
color += sun_glow_color.rgb * glow * sun_glow_intensity;
}
if (dawn_dusk_enabled && dawn_dusk_blend > 0.0) {
float horizon_factor = 1.0 - abs(y);
horizon_factor = pow(horizon_factor, 2.0);
color = mix(color, dawn_dusk_color.rgb, horizon_factor * dawn_dusk_intensity * dawn_dusk_blend);
}
if (stars_visibility > 0.0 && y > 0.0) {
float star_value = stars(dir) * stars_visibility;
color += stars_tint.rgb * star_value;
}
COLOR = color;
}
"""
return shader
func _update_sky_material() -> void:
if _sky_material == null:
return
if skybox_texture:
_sky_material.set_shader_parameter("skybox_enabled", true)
_sky_material.set_shader_parameter("skybox_texture", skybox_texture)
_sky_material.set_shader_parameter("skybox_blend", skybox_blend)
else:
_sky_material.set_shader_parameter("skybox_enabled", false)
_sky_material.set_shader_parameter("skybox_blend", skybox_blend)
_sky_material.set_shader_parameter("sky_top_color", sky_top_color)
_sky_material.set_shader_parameter("sky_horizon_color", sky_horizon_color)
_sky_material.set_shader_parameter("sky_bottom_color", sky_bottom_color)
_sky_material.set_shader_parameter("gradient_intensity", gradient_intensity)
_sky_material.set_shader_parameter("sun_glow_enabled", sun_glow_enabled and effects_enabled)
_sky_material.set_shader_parameter("sun_direction", sun_direction)
_sky_material.set_shader_parameter("sun_glow_color", sun_glow_color)
_sky_material.set_shader_parameter("sun_glow_intensity", sun_glow_intensity)
_sky_material.set_shader_parameter("sun_glow_size", sun_glow_size)
_sky_material.set_shader_parameter("sun_glow_falloff", sun_glow_falloff)
_sky_material.set_shader_parameter("dawn_dusk_enabled", dawn_dusk_enabled and effects_enabled)
_sky_material.set_shader_parameter("dawn_dusk_blend", dawn_dusk_blend)
_sky_material.set_shader_parameter("dawn_dusk_intensity", dawn_dusk_intensity)
func set_sky_colors(top: Color, horizon: Color, bottom: Color) -> void:
sky_top_color = top
sky_horizon_color = horizon
sky_bottom_color = bottom
_update_sky_material()
func set_fog_settings(color: Color, density: float) -> void:
fog_color = color
fog_density = density
_update_fog()
func set_sun_glow(direction: Vector3, color: Color, intensity: float, size: float) -> void:
sun_direction = direction.normalized()
sun_glow_color = color
sun_glow_intensity = intensity
sun_glow_size = size
_update_sky_material()
func set_dawn_dusk_blend(blend: float) -> void:
dawn_dusk_blend = clampf(blend, 0.0, 1.0)
_update_sky_material()
func set_stars_visibility(visibility: float, tint: Color = Color.WHITE) -> void:
if _sky_material:
_sky_material.set_shader_parameter("stars_visibility", visibility)
_sky_material.set_shader_parameter("stars_tint", tint)
func set_effects_enabled(enabled: bool) -> void:
effects_enabled = enabled
_update_fog()
_update_sky_material()
func apply_config(config: SkyConfig) -> void:
sky_top_color = config.sky_top_color
sky_horizon_color = config.sky_horizon_color
sky_bottom_color = config.sky_bottom_color
fog_color = config.fog_color
fog_density = config.fog_density
sun_glow_intensity = config.sun_glow_intensity
sun_glow_size = config.sun_glow_size
gradient_intensity = config.gradient_intensity
_update_fog()
_update_sky_material()
set_stars_visibility(config.stars_visibility, config.stars_tint)

View File

@@ -0,0 +1 @@
uid://cwqg0bg2m2bme

View File

@@ -0,0 +1,93 @@
@tool
class_name ProceduralCloudTexture
extends Resource
@export var size: int = 512
@export var octaves: int = 6
@export var persistence: float = 0.5
@export var lacunarity: float = 2.0
@export var scale: float = 4.0
@export var seed_value: int = 0
@export var cloud_threshold: float = 0.4
@export var cloud_softness: float = 0.3
var _noise: FastNoiseLite
var _texture: ImageTexture
func _init() -> void:
_setup_noise()
func _setup_noise() -> void:
_noise = FastNoiseLite.new()
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
_noise.seed = seed_value
_noise.frequency = 1.0 / (scale * 64.0)
_noise.fractal_type = FastNoiseLite.FRACTAL_FBM
_noise.fractal_octaves = octaves
_noise.fractal_lacunarity = lacunarity
_noise.fractal_gain = persistence
func generate() -> ImageTexture:
_setup_noise()
var image := Image.create(size, size, false, Image.FORMAT_RGBA8)
for y in size:
for x in size:
var noise_value := (_noise.get_noise_2d(float(x), float(y)) + 1.0) * 0.5
var cloud_value := smoothstep(cloud_threshold - cloud_softness, cloud_threshold + cloud_softness, noise_value)
var color := Color(cloud_value, cloud_value, cloud_value, cloud_value)
image.set_pixel(x, y, color)
_texture = ImageTexture.create_from_image(image)
return _texture
func get_texture() -> ImageTexture:
if _texture == null:
return generate()
return _texture
static func create_default_cloud_texture(layer_type: int = 0) -> ImageTexture:
var generator := ProceduralCloudTexture.new()
generator.size = 512
match layer_type:
0: # Low clouds - larger, denser
generator.scale = 3.0
generator.octaves = 4
generator.cloud_threshold = 0.35
generator.cloud_softness = 0.25
1: # Mid clouds - medium
generator.scale = 4.0
generator.octaves = 5
generator.cloud_threshold = 0.4
generator.cloud_softness = 0.3
2: # High clouds - wispy
generator.scale = 6.0
generator.octaves = 6
generator.cloud_threshold = 0.5
generator.cloud_softness = 0.35
3: # Wisps - very thin
generator.scale = 8.0
generator.octaves = 7
generator.cloud_threshold = 0.55
generator.cloud_softness = 0.4
return generator.generate()
static func create_shadow_texture() -> ImageTexture:
var generator := ProceduralCloudTexture.new()
generator.size = 256
generator.scale = 4.0
generator.octaves = 4
generator.cloud_threshold = 0.3
generator.cloud_softness = 0.4
return generator.generate()

View File

@@ -0,0 +1 @@
uid://dyjrdeqft0xlk

View File

@@ -0,0 +1,220 @@
@tool
class_name ShadowProxySystem
extends Node3D
@export_group("Shadow Settings")
@export var enabled: bool = true:
set(value):
enabled = value
if _shadow_mesh:
_shadow_mesh.visible = enabled
@export var shadow_texture: Texture2D:
set(value):
shadow_texture = value
_update_shadow_texture()
@export var shadow_opacity: float = 0.3:
set(value):
shadow_opacity = clampf(value, 0.0, 1.0)
_update_shadow_material()
@export var shadow_color: Color = Color(0.0, 0.0, 0.1):
set(value):
shadow_color = value
_update_shadow_material()
@export_group("Movement")
@export var flow_direction: Vector2 = Vector2(1.0, 0.0)
@export var flow_speed: float = 5.0
@export var sync_with_clouds: bool = true
@export_group("Projection")
@export var projection_size: float = 500.0:
set(value):
projection_size = value
_update_projection_size()
@export var projection_height: float = 0.1
@export var tiling: Vector2 = Vector2(2.0, 2.0):
set(value):
tiling = value
_update_shadow_material()
@export_group("Blur")
@export var blur_enabled: bool = true
@export var blur_amount: float = 0.5:
set(value):
blur_amount = clampf(value, 0.0, 1.0)
_update_shadow_material()
@export_group("Performance")
@export var follow_camera: bool = true
@export var update_distance: float = 50.0
var _shadow_mesh: MeshInstance3D
var _shadow_material: ShaderMaterial
var _uv_offset: Vector2 = Vector2.ZERO
var _last_camera_position: Vector3 = Vector3.ZERO
func _ready() -> void:
_create_shadow_mesh()
_create_shadow_material()
_update_shadow_material()
func _process(delta: float) -> void:
if not enabled:
return
_update_flow(delta)
if follow_camera:
_follow_camera()
func _create_shadow_mesh() -> void:
if _shadow_mesh:
_shadow_mesh.queue_free()
_shadow_mesh = MeshInstance3D.new()
_shadow_mesh.name = "CloudShadowMesh"
var plane := PlaneMesh.new()
plane.size = Vector2(projection_size, projection_size)
_shadow_mesh.mesh = plane
_shadow_mesh.position.y = projection_height
_shadow_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
add_child(_shadow_mesh)
func _create_shadow_material() -> void:
_shadow_material = ShaderMaterial.new()
_shadow_material.shader = _get_shadow_shader()
if _shadow_mesh:
_shadow_mesh.material_override = _shadow_material
func _get_shadow_shader() -> Shader:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode unshaded, depth_draw_opaque, cull_disabled, shadows_disabled;
uniform sampler2D shadow_texture : source_color, filter_linear_mipmap, repeat_enable;
uniform vec4 shadow_color : source_color = vec4(0.0, 0.0, 0.1, 1.0);
uniform float opacity = 0.3;
uniform vec2 tiling = vec2(2.0, 2.0);
uniform vec2 uv_offset = vec2(0.0, 0.0);
uniform float blur_amount = 0.5;
uniform float edge_fade = 0.1;
varying vec2 world_uv;
void vertex() {
world_uv = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
}
void fragment() {
vec2 uv = world_uv * tiling * 0.001 + uv_offset;
vec4 shadow_sample;
if (blur_amount > 0.01) {
float blur = blur_amount * 0.01;
shadow_sample = texture(shadow_texture, uv) * 0.25;
shadow_sample += texture(shadow_texture, uv + vec2(blur, 0.0)) * 0.125;
shadow_sample += texture(shadow_texture, uv - vec2(blur, 0.0)) * 0.125;
shadow_sample += texture(shadow_texture, uv + vec2(0.0, blur)) * 0.125;
shadow_sample += texture(shadow_texture, uv - vec2(0.0, blur)) * 0.125;
shadow_sample += texture(shadow_texture, uv + vec2(blur, blur)) * 0.0625;
shadow_sample += texture(shadow_texture, uv - vec2(blur, blur)) * 0.0625;
shadow_sample += texture(shadow_texture, uv + vec2(blur, -blur)) * 0.0625;
shadow_sample += texture(shadow_texture, uv + vec2(-blur, blur)) * 0.0625;
} else {
shadow_sample = texture(shadow_texture, uv);
}
float shadow_value = shadow_sample.r;
vec2 local_uv = fract(world_uv * 0.001);
float edge = smoothstep(0.0, edge_fade, local_uv.x);
edge *= smoothstep(0.0, edge_fade, local_uv.y);
edge *= smoothstep(0.0, edge_fade, 1.0 - local_uv.x);
edge *= smoothstep(0.0, edge_fade, 1.0 - local_uv.y);
ALBEDO = shadow_color.rgb;
ALPHA = shadow_value * opacity * edge;
}
"""
return shader
func _update_shadow_material() -> void:
if _shadow_material == null:
return
_shadow_material.set_shader_parameter("shadow_color", shadow_color)
_shadow_material.set_shader_parameter("opacity", shadow_opacity)
_shadow_material.set_shader_parameter("tiling", tiling)
_shadow_material.set_shader_parameter("blur_amount", blur_amount if blur_enabled else 0.0)
func _update_shadow_texture() -> void:
if _shadow_material and shadow_texture:
_shadow_material.set_shader_parameter("shadow_texture", shadow_texture)
func _update_projection_size() -> void:
if _shadow_mesh and _shadow_mesh.mesh is PlaneMesh:
var plane := _shadow_mesh.mesh as PlaneMesh
plane.size = Vector2(projection_size, projection_size)
func _update_flow(delta: float) -> void:
_uv_offset += flow_direction * flow_speed * delta * 0.001
_uv_offset.x = fmod(_uv_offset.x, 1.0)
_uv_offset.y = fmod(_uv_offset.y, 1.0)
if _shadow_material:
_shadow_material.set_shader_parameter("uv_offset", _uv_offset)
func _follow_camera() -> void:
var camera := get_viewport().get_camera_3d()
if camera == null:
return
var camera_pos := camera.global_position
var distance := camera_pos.distance_to(_last_camera_position)
if distance >= update_distance:
_last_camera_position = camera_pos
global_position.x = camera_pos.x
global_position.z = camera_pos.z
func set_flow(direction: Vector2, speed: float) -> void:
flow_direction = direction.normalized()
flow_speed = speed
func set_shadow_appearance(color: Color, opacity_value: float, blur: float) -> void:
shadow_color = color
shadow_opacity = opacity_value
blur_amount = blur
_update_shadow_material()
func sync_with_cloud_system(cloud_system: CloudSystem2D) -> void:
if sync_with_clouds and cloud_system:
flow_direction = cloud_system.global_wind_direction
flow_speed = cloud_system.global_wind_speed * 5.0
func set_cloud_coverage(coverage: float) -> void:
shadow_opacity = coverage * 0.5
_update_shadow_material()

View File

@@ -0,0 +1 @@
uid://hvs2g6acqryq

View File

@@ -0,0 +1,171 @@
@tool
class_name SkyDebugPanel
extends Control
@export var dynamic_sky: DynamicSkyRoot
var _panel: PanelContainer
var _vbox: VBoxContainer
var _time_label: Label
var _weather_label: Label
var _cloud_label: Label
var _seed_label: Label
var _time_slider: HSlider
var _weather_dropdown: OptionButton
var _pause_button: Button
func _ready() -> void:
_create_ui()
_connect_signals()
func _process(_delta: float) -> void:
if dynamic_sky == null:
return
_update_labels()
func _create_ui() -> void:
_panel = PanelContainer.new()
_panel.name = "DebugPanel"
_panel.custom_minimum_size = Vector2(300, 200)
add_child(_panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 10)
margin.add_theme_constant_override("margin_right", 10)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_bottom", 10)
_panel.add_child(margin)
_vbox = VBoxContainer.new()
_vbox.add_theme_constant_override("separation", 8)
margin.add_child(_vbox)
var title := Label.new()
title.text = "Dynamic Sky Debug"
title.add_theme_font_size_override("font_size", 18)
_vbox.add_child(title)
_vbox.add_child(HSeparator.new())
_time_label = Label.new()
_time_label.text = "Time: 00:00 (0.00)"
_vbox.add_child(_time_label)
var time_hbox := HBoxContainer.new()
_vbox.add_child(time_hbox)
var time_label := Label.new()
time_label.text = "Time:"
time_label.custom_minimum_size.x = 60
time_hbox.add_child(time_label)
_time_slider = HSlider.new()
_time_slider.min_value = 0.0
_time_slider.max_value = 1.0
_time_slider.step = 0.001
_time_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
time_hbox.add_child(_time_slider)
_pause_button = Button.new()
_pause_button.text = "Pause"
_pause_button.toggle_mode = true
_vbox.add_child(_pause_button)
_vbox.add_child(HSeparator.new())
_weather_label = Label.new()
_weather_label.text = "Weather: Clear"
_vbox.add_child(_weather_label)
var weather_hbox := HBoxContainer.new()
_vbox.add_child(weather_hbox)
var weather_label := Label.new()
weather_label.text = "Set:"
weather_label.custom_minimum_size.x = 60
weather_hbox.add_child(weather_label)
_weather_dropdown = OptionButton.new()
_weather_dropdown.size_flags_horizontal = Control.SIZE_EXPAND_FILL
for state_name in WeatherProfile.WeatherState.keys():
_weather_dropdown.add_item(state_name)
weather_hbox.add_child(_weather_dropdown)
_vbox.add_child(HSeparator.new())
_cloud_label = Label.new()
_cloud_label.text = "Clouds: 0%"
_vbox.add_child(_cloud_label)
_seed_label = Label.new()
_seed_label.text = "Seed: 0"
_vbox.add_child(_seed_label)
var meteor_button := Button.new()
meteor_button.text = "Trigger Meteor Shower"
meteor_button.pressed.connect(_on_meteor_pressed)
_vbox.add_child(meteor_button)
func _connect_signals() -> void:
_time_slider.value_changed.connect(_on_time_slider_changed)
_pause_button.toggled.connect(_on_pause_toggled)
_weather_dropdown.item_selected.connect(_on_weather_selected)
func _update_labels() -> void:
if dynamic_sky == null:
return
var time := dynamic_sky.get_time_of_day()
var hours := int(time * 24.0)
var minutes := int(fmod(time * 24.0 * 60.0, 60.0))
_time_label.text = "Time: %02d:%02d (%.3f)" % [hours, minutes, time]
if not _time_slider.has_focus():
_time_slider.set_value_no_signal(time)
var weather := dynamic_sky.get_current_weather()
var weather_name: String = WeatherProfile.WeatherState.keys()[weather]
_weather_label.text = "Weather: %s" % weather_name
if dynamic_sky.weather_controller:
var coverage := dynamic_sky.weather_controller.get_cloud_coverage()
_cloud_label.text = "Clouds: %d%%" % int(coverage * 100)
if dynamic_sky.weather_controller.is_transitioning():
var progress := dynamic_sky.weather_controller.get_transition_progress()
_weather_label.text += " (transitioning %.0f%%)" % (progress * 100)
_seed_label.text = "Seed: %d" % dynamic_sky.deterministic_seed
if dynamic_sky.day_night_controller:
_pause_button.set_pressed_no_signal(dynamic_sky.day_night_controller.paused)
func _on_time_slider_changed(value: float) -> void:
if dynamic_sky:
dynamic_sky.set_time_of_day(value)
func _on_pause_toggled(pressed: bool) -> void:
if dynamic_sky:
if pressed:
dynamic_sky.pause_time()
else:
dynamic_sky.resume_time()
_pause_button.text = "Resume" if pressed else "Pause"
func _on_weather_selected(index: int) -> void:
if dynamic_sky:
var state: WeatherProfile.WeatherState = index as WeatherProfile.WeatherState
dynamic_sky.set_weather(state)
func _on_meteor_pressed() -> void:
if dynamic_sky:
dynamic_sky.trigger_meteor_shower(30.0)

View File

@@ -0,0 +1 @@
uid://cimhpqq1qo80b

View File

@@ -0,0 +1,131 @@
@tool
class_name SkyboxPresetLibrary
extends Node
signal preset_changed(preset: SkyPreset)
signal preset_applied(preset_name: String)
signal preset_reverted(preset_name: String)
@export var presets: Array[SkyPreset] = []
@export var current_preset_index: int = 0:
set(value):
current_preset_index = clampi(value, 0, maxi(0, presets.size() - 1))
if current_preset_index < presets.size():
_on_preset_selected(presets[current_preset_index])
var _base_preset: SkyPreset
var _runtime_overrides: SkyConfig
func _ready() -> void:
if presets.is_empty():
_initialize_default_presets()
if current_preset_index < presets.size():
_base_preset = presets[current_preset_index]
func _initialize_default_presets() -> void:
presets.clear()
presets.append(SkyPreset.create_clear_day())
presets.append(SkyPreset.create_sunset())
presets.append(SkyPreset.create_night())
presets.append(SkyPreset.create_overcast())
presets.append(SkyPreset.create_storm())
presets.append(SkyPreset.create_dawn())
func get_preset_names() -> PackedStringArray:
var names := PackedStringArray()
for preset in presets:
names.append(preset.preset_name)
return names
func get_current_preset() -> SkyPreset:
if current_preset_index < presets.size():
return presets[current_preset_index]
return null
func get_preset_by_name(preset_name: String) -> SkyPreset:
for preset in presets:
if preset.preset_name == preset_name:
return preset
return null
func get_preset_by_type(preset_type: SkyPreset.PresetType) -> SkyPreset:
for preset in presets:
if preset.preset_type == preset_type:
return preset
return null
func apply_preset(preset_name: String) -> bool:
var preset := get_preset_by_name(preset_name)
if preset == null:
push_warning("SkyboxPresetLibrary: Preset '%s' not found" % preset_name)
return false
for i in presets.size():
if presets[i] == preset:
current_preset_index = i
break
_base_preset = preset
_runtime_overrides = null
preset_applied.emit(preset_name)
preset_changed.emit(preset)
return true
func apply_preset_by_type(preset_type: SkyPreset.PresetType) -> bool:
var preset := get_preset_by_type(preset_type)
if preset == null:
push_warning("SkyboxPresetLibrary: Preset type '%s' not found" % SkyPreset.PresetType.keys()[preset_type])
return false
return apply_preset(preset.preset_name)
func revert_to_preset() -> void:
_runtime_overrides = null
if _base_preset:
preset_reverted.emit(_base_preset.preset_name)
preset_changed.emit(_base_preset)
func set_runtime_override(config: SkyConfig) -> void:
_runtime_overrides = config
func get_effective_config() -> SkyConfig:
if _runtime_overrides != null:
return _runtime_overrides
if _base_preset != null and _base_preset.config != null:
return _base_preset.config
return SkyConfig.new()
func has_runtime_overrides() -> bool:
return _runtime_overrides != null
func add_preset(preset: SkyPreset) -> void:
if preset not in presets:
presets.append(preset)
func remove_preset(preset_name: String) -> bool:
for i in presets.size():
if presets[i].preset_name == preset_name:
presets.remove_at(i)
if current_preset_index >= presets.size():
current_preset_index = maxi(0, presets.size() - 1)
return true
return false
func _on_preset_selected(preset: SkyPreset) -> void:
_base_preset = preset
_runtime_overrides = null
preset_changed.emit(preset)

View File

@@ -0,0 +1 @@
uid://cb8c6ug44jx4t

View File

@@ -0,0 +1,299 @@
@tool
class_name VFXController
extends Node3D
signal shooting_star_spawned(position: Vector3, direction: Vector3)
signal meteor_shower_started()
signal meteor_shower_ended()
@export_group("Shooting Stars")
@export var shooting_stars_enabled: bool = true
@export var shooting_star_frequency: float = 0.05
@export var min_spawn_interval: float = 5.0
@export var max_spawn_interval: float = 30.0
@export_group("Shooting Star Appearance")
@export var trail_length: float = 2.0
@export var trail_width: float = 0.05
@export var star_brightness: float = 1.5
@export var star_color: Color = Color(1.0, 1.0, 0.9)
@export var trail_fade_time: float = 0.5
@export_group("Meteor Shower")
@export var meteor_shower_active: bool = false
@export var meteor_shower_intensity: float = 1.0
@export var meteors_per_second: float = 3.0
@export var meteor_shower_duration: float = 60.0
@export_group("Meteor Appearance")
@export var meteor_trail_length: float = 4.0
@export var meteor_brightness: float = 2.0
@export var meteor_color: Color = Color(1.0, 0.8, 0.5)
@export_group("Spawn Area")
@export var spawn_radius: float = 500.0
@export var spawn_height_min: float = 100.0
@export var spawn_height_max: float = 200.0
@export var spawn_direction_variance: float = 30.0
@export_group("Audio")
@export var meteor_shower_audio_event: String = ""
var _spawn_timer: float = 0.0
var _next_spawn_time: float = 0.0
var _meteor_timer: float = 0.0
var _meteor_spawn_accumulator: float = 0.0
var _meteor_shower_elapsed: float = 0.0
var _active_stars: Array[Dictionary] = []
var _star_mesh: MeshInstance3D
var _star_material: ShaderMaterial
var _is_night: bool = false
func _ready() -> void:
_setup_star_mesh()
_schedule_next_shooting_star()
func _process(delta: float) -> void:
if not _is_night:
return
if shooting_stars_enabled:
_process_shooting_stars(delta)
if meteor_shower_active:
_process_meteor_shower(delta)
_update_active_stars(delta)
func _setup_star_mesh() -> void:
_star_material = ShaderMaterial.new()
_star_material.shader = _get_star_shader()
_star_material.set_shader_parameter("color", star_color)
_star_material.set_shader_parameter("brightness", star_brightness)
func _get_star_shader() -> Shader:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode unshaded, depth_draw_never, cull_disabled, blend_add;
uniform vec4 color : source_color = vec4(1.0, 1.0, 0.9, 1.0);
uniform float brightness = 1.5;
uniform float fade = 1.0;
uniform float trail_progress = 0.0;
void vertex() {
VERTEX.y *= mix(0.1, 1.0, trail_progress);
}
void fragment() {
float gradient = UV.y;
float alpha = gradient * fade;
ALBEDO = color.rgb * brightness;
ALPHA = alpha * color.a;
}
"""
return shader
func _schedule_next_shooting_star() -> void:
_next_spawn_time = randf_range(min_spawn_interval, max_spawn_interval)
_next_spawn_time /= maxf(0.01, shooting_star_frequency)
_spawn_timer = 0.0
func _process_shooting_stars(delta: float) -> void:
_spawn_timer += delta
if _spawn_timer >= _next_spawn_time:
_spawn_shooting_star()
_schedule_next_shooting_star()
func _process_meteor_shower(delta: float) -> void:
_meteor_shower_elapsed += delta
if _meteor_shower_elapsed >= meteor_shower_duration:
stop_meteor_shower()
return
_meteor_spawn_accumulator += meteors_per_second * meteor_shower_intensity * delta
while _meteor_spawn_accumulator >= 1.0:
_meteor_spawn_accumulator -= 1.0
_spawn_meteor()
func _spawn_shooting_star() -> void:
var spawn_pos := _get_random_sky_position()
var direction := _get_random_fall_direction()
var star_data := {
"position": spawn_pos,
"direction": direction,
"speed": randf_range(50.0, 100.0),
"lifetime": 0.0,
"max_lifetime": trail_fade_time + randf_range(0.2, 0.5),
"trail_length": trail_length * randf_range(0.8, 1.2),
"brightness": star_brightness * randf_range(0.8, 1.2),
"color": star_color,
"mesh": _create_star_trail(spawn_pos, direction),
"is_meteor": false
}
_active_stars.append(star_data)
shooting_star_spawned.emit(spawn_pos, direction)
func _spawn_meteor() -> void:
var spawn_pos := _get_random_sky_position()
var direction := _get_random_fall_direction()
var star_data := {
"position": spawn_pos,
"direction": direction,
"speed": randf_range(80.0, 150.0),
"lifetime": 0.0,
"max_lifetime": trail_fade_time + randf_range(0.3, 0.7),
"trail_length": meteor_trail_length * randf_range(0.8, 1.2),
"brightness": meteor_brightness * randf_range(0.8, 1.2),
"color": meteor_color,
"mesh": _create_star_trail(spawn_pos, direction, true),
"is_meteor": true
}
_active_stars.append(star_data)
func _get_random_sky_position() -> Vector3:
var angle := randf() * TAU
var radius := randf_range(spawn_radius * 0.3, spawn_radius)
var height := randf_range(spawn_height_min, spawn_height_max)
return Vector3(
cos(angle) * radius,
height,
sin(angle) * radius
)
func _get_random_fall_direction() -> Vector3:
var base_dir := Vector3(-0.5, -0.8, -0.3).normalized()
var variance := deg_to_rad(spawn_direction_variance)
var rotation := Basis(Vector3.UP, randf() * TAU)
rotation = rotation.rotated(Vector3.RIGHT, randf_range(-variance, variance))
return (rotation * base_dir).normalized()
func _create_star_trail(position: Vector3, direction: Vector3, is_meteor: bool = false) -> MeshInstance3D:
var mesh_instance := MeshInstance3D.new()
var length := meteor_trail_length if is_meteor else trail_length
var width := trail_width * (1.5 if is_meteor else 1.0)
var quad := QuadMesh.new()
quad.size = Vector2(width, length)
mesh_instance.mesh = quad
var material := _star_material.duplicate() as ShaderMaterial
material.set_shader_parameter("color", meteor_color if is_meteor else star_color)
material.set_shader_parameter("brightness", meteor_brightness if is_meteor else star_brightness)
mesh_instance.material_override = material
mesh_instance.position = position
mesh_instance.look_at(position + direction, Vector3.UP)
mesh_instance.rotate_object_local(Vector3.RIGHT, PI / 2.0)
add_child(mesh_instance)
return mesh_instance
func _update_active_stars(delta: float) -> void:
var stars_to_remove: Array[int] = []
for i in _active_stars.size():
var star: Dictionary = _active_stars[i]
star["lifetime"] += delta
var progress: float = star["lifetime"] / star["max_lifetime"]
if progress >= 1.0:
stars_to_remove.append(i)
continue
var mesh: MeshInstance3D = star["mesh"]
if is_instance_valid(mesh):
star["position"] += star["direction"] * star["speed"] * delta
mesh.position = star["position"]
var fade := 1.0 - smoothstep(0.5, 1.0, progress)
var material := mesh.material_override as ShaderMaterial
if material:
material.set_shader_parameter("fade", fade)
material.set_shader_parameter("trail_progress", minf(progress * 3.0, 1.0))
for i in range(stars_to_remove.size() - 1, -1, -1):
var idx: int = stars_to_remove[i]
var star: Dictionary = _active_stars[idx]
var mesh: MeshInstance3D = star["mesh"]
if is_instance_valid(mesh):
mesh.queue_free()
_active_stars.remove_at(idx)
func set_night_mode(is_night: bool) -> void:
_is_night = is_night
if not is_night:
_clear_all_stars()
func _clear_all_stars() -> void:
for star in _active_stars:
var mesh: MeshInstance3D = star["mesh"]
if is_instance_valid(mesh):
mesh.queue_free()
_active_stars.clear()
func start_meteor_shower(duration: float = -1.0) -> void:
if duration > 0:
meteor_shower_duration = duration
meteor_shower_active = true
_meteor_shower_elapsed = 0.0
_meteor_spawn_accumulator = 0.0
meteor_shower_started.emit()
if meteor_shower_audio_event != "":
pass
func stop_meteor_shower() -> void:
meteor_shower_active = false
_meteor_shower_elapsed = 0.0
meteor_shower_ended.emit()
func trigger_shooting_star() -> void:
if _is_night:
_spawn_shooting_star()
func set_shooting_star_appearance(color: Color, brightness: float, length: float) -> void:
star_color = color
star_brightness = brightness
trail_length = length
func set_meteor_appearance(color: Color, brightness: float, length: float) -> void:
meteor_color = color
meteor_brightness = brightness
meteor_trail_length = length

View File

@@ -0,0 +1 @@
uid://cnvj3m5fpsdtj

View File

@@ -0,0 +1,278 @@
@tool
class_name WeatherController
extends Node
signal weather_state_changed(old_state: WeatherProfile.WeatherState, new_state: WeatherProfile.WeatherState)
signal weather_transition_started(from: WeatherProfile.WeatherState, to: WeatherProfile.WeatherState)
signal weather_transition_completed(state: WeatherProfile.WeatherState)
signal weather_state_entered(state: WeatherProfile.WeatherState)
signal weather_state_exited(state: WeatherProfile.WeatherState)
@export_group("Weather Profiles")
@export var weather_profiles: Array[WeatherProfile] = []
@export var current_weather: WeatherProfile.WeatherState = WeatherProfile.WeatherState.CLEAR:
set(value):
if value != current_weather:
_request_weather_change(value)
@export_group("Transition Settings")
@export var default_transition_duration: float = 5.0
@export var transition_curve: Curve
@export_group("Random Weather")
@export var enable_random_weather: bool = false
@export var min_weather_duration: float = 60.0
@export var max_weather_duration: float = 300.0
@export var snow_enabled: bool = true
@export_group("Scripted Timeline")
@export var use_scripted_timeline: bool = false
@export var weather_timeline: Array[Dictionary] = []
var _current_profile: WeatherProfile
var _target_profile: WeatherProfile
var _transition_weight: float = 1.0
var _is_transitioning: bool = false
var _transition_duration: float = 0.0
var _transition_elapsed: float = 0.0
var _random_weather_timer: float = 0.0
var _next_weather_change: float = 0.0
var _timeline_index: int = 0
var _timeline_timer: float = 0.0
func _ready() -> void:
if weather_profiles.is_empty():
_initialize_default_profiles()
if transition_curve == null:
transition_curve = Curve.new()
transition_curve.add_point(Vector2(0.0, 0.0))
transition_curve.add_point(Vector2(0.5, 0.5))
transition_curve.add_point(Vector2(1.0, 1.0))
_current_profile = get_profile_for_state(current_weather)
_target_profile = _current_profile
_schedule_next_random_weather()
func _process(delta: float) -> void:
if _is_transitioning:
_process_transition(delta)
if enable_random_weather and not use_scripted_timeline:
_process_random_weather(delta)
if use_scripted_timeline:
_process_scripted_timeline(delta)
func _initialize_default_profiles() -> void:
weather_profiles.clear()
weather_profiles.append(WeatherProfile.create_clear())
weather_profiles.append(WeatherProfile.create_cloudy())
weather_profiles.append(WeatherProfile.create_overcast())
weather_profiles.append(WeatherProfile.create_rain())
weather_profiles.append(WeatherProfile.create_storm())
weather_profiles.append(WeatherProfile.create_snow())
func _process_transition(delta: float) -> void:
_transition_elapsed += delta
var t := clampf(_transition_elapsed / _transition_duration, 0.0, 1.0)
if transition_curve:
_transition_weight = transition_curve.sample(t)
else:
_transition_weight = t
if t >= 1.0:
_complete_transition()
func _complete_transition() -> void:
_is_transitioning = false
_transition_weight = 1.0
_current_profile = _target_profile
var new_state := _current_profile.weather_state
weather_transition_completed.emit(new_state)
weather_state_entered.emit(new_state)
func _process_random_weather(delta: float) -> void:
_random_weather_timer += delta
if _random_weather_timer >= _next_weather_change:
_random_weather_timer = 0.0
_trigger_random_weather_change()
_schedule_next_random_weather()
func _schedule_next_random_weather() -> void:
_next_weather_change = randf_range(min_weather_duration, max_weather_duration)
func _trigger_random_weather_change() -> void:
var available_states: Array[WeatherProfile.WeatherState] = []
for state in WeatherProfile.WeatherState.values():
if state == current_weather:
continue
if state == WeatherProfile.WeatherState.SNOW and not snow_enabled:
continue
available_states.append(state)
if available_states.is_empty():
return
var new_state: WeatherProfile.WeatherState = available_states.pick_random()
set_weather(new_state)
func _process_scripted_timeline(delta: float) -> void:
if weather_timeline.is_empty():
return
_timeline_timer += delta
if _timeline_index < weather_timeline.size():
var entry: Dictionary = weather_timeline[_timeline_index]
var trigger_time: float = entry.get("time", 0.0)
if _timeline_timer >= trigger_time:
var state_name: String = entry.get("state", "CLEAR")
var duration: float = entry.get("duration", default_transition_duration)
if WeatherProfile.WeatherState.has(state_name):
var state: WeatherProfile.WeatherState = WeatherProfile.WeatherState.get(state_name)
set_weather(state, duration)
_timeline_index += 1
func _request_weather_change(new_state: WeatherProfile.WeatherState) -> void:
var profile := get_profile_for_state(new_state)
if profile == null:
push_warning("WeatherController: No profile for state %s" % WeatherProfile.WeatherState.keys()[new_state])
return
set_weather(new_state, profile.default_transition_duration)
func get_profile_for_state(state: WeatherProfile.WeatherState) -> WeatherProfile:
for profile in weather_profiles:
if profile.weather_state == state:
return profile
return null
func set_weather(state: WeatherProfile.WeatherState, transition_duration: float = -1.0) -> void:
var profile := get_profile_for_state(state)
if profile == null:
push_warning("WeatherController: No profile for state %s" % WeatherProfile.WeatherState.keys()[state])
return
if _current_profile:
weather_state_exited.emit(_current_profile.weather_state)
var old_state := current_weather
current_weather = state
_target_profile = profile
if transition_duration < 0:
transition_duration = profile.default_transition_duration
_transition_duration = transition_duration
_transition_elapsed = 0.0
_is_transitioning = true
_transition_weight = 0.0
weather_transition_started.emit(old_state, state)
weather_state_changed.emit(old_state, state)
func set_weather_immediate(state: WeatherProfile.WeatherState) -> void:
var profile := get_profile_for_state(state)
if profile == null:
return
if _current_profile:
weather_state_exited.emit(_current_profile.weather_state)
var old_state := current_weather
current_weather = state
_current_profile = profile
_target_profile = profile
_is_transitioning = false
_transition_weight = 1.0
weather_state_changed.emit(old_state, state)
weather_state_entered.emit(state)
func get_current_profile() -> WeatherProfile:
if _is_transitioning and _current_profile and _target_profile:
return _current_profile.lerp_to(_target_profile, _transition_weight)
return _current_profile
func get_cloud_density() -> float:
var profile := get_current_profile()
return profile.cloud_density if profile else 0.2
func get_cloud_opacity() -> float:
var profile := get_current_profile()
return profile.cloud_opacity if profile else 0.7
func get_cloud_coverage() -> float:
var profile := get_current_profile()
return profile.cloud_coverage if profile else 0.3
func get_cloud_color() -> Color:
var profile := get_current_profile()
return profile.cloud_color if profile else Color.WHITE
func get_wind_speed() -> float:
var profile := get_current_profile()
return profile.wind_speed if profile else 1.0
func get_wind_direction() -> Vector2:
var profile := get_current_profile()
return profile.wind_direction if profile else Vector2(1.0, 0.0)
func get_rain_intensity() -> float:
var profile := get_current_profile()
return profile.rain_intensity if profile else 0.0
func get_snow_intensity() -> float:
var profile := get_current_profile()
return profile.snow_intensity if profile else 0.0
func is_lightning_active() -> bool:
var profile := get_current_profile()
return profile.lightning_enabled if profile else false
func get_lightning_frequency() -> float:
var profile := get_current_profile()
return profile.lightning_frequency if profile else 0.0
func get_transition_progress() -> float:
return _transition_weight
func is_transitioning() -> bool:
return _is_transitioning
func reset_timeline() -> void:
_timeline_index = 0
_timeline_timer = 0.0

View File

@@ -0,0 +1 @@
uid://blyx8v2gq8nlw

View File

@@ -0,0 +1,277 @@
@tool
class_name WeatherKillVolume
extends Area3D
enum VolumeShape {
BOX,
SPHERE,
CYLINDER
}
@export_group("Volume Settings")
@export var volume_shape: VolumeShape = VolumeShape.BOX:
set(value):
volume_shape = value
_update_collision_shape()
@export var volume_size: Vector3 = Vector3(10.0, 5.0, 10.0):
set(value):
volume_size = value
_update_collision_shape()
@export var volume_radius: float = 5.0:
set(value):
volume_radius = value
_update_collision_shape()
@export_group("Kill Behavior")
@export var enabled: bool = true
@export var priority: int = 0
@export var instant_kill: bool = true
@export var fade_out_margin: float = 1.0
@export var fade_out_duration: float = 0.3
@export_group("Particle Types")
@export var block_rain: bool = true
@export var block_snow: bool = true
@export var block_all_precipitation: bool = true
@export_group("Debug")
@export var show_debug_gizmo: bool = true
@export var debug_color: Color = Color(0.2, 0.5, 1.0, 0.3)
var _collision_shape: CollisionShape3D
var _particles_inside: Dictionary = {}
func _ready() -> void:
_setup_collision()
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
area_entered.connect(_on_area_entered)
area_exited.connect(_on_area_exited)
func _setup_collision() -> void:
_collision_shape = CollisionShape3D.new()
_collision_shape.name = "KillVolumeShape"
add_child(_collision_shape)
_update_collision_shape()
collision_layer = 0
collision_mask = 1 << 20
monitorable = true
monitoring = true
func _update_collision_shape() -> void:
if _collision_shape == null:
return
var shape: Shape3D
match volume_shape:
VolumeShape.BOX:
var box := BoxShape3D.new()
box.size = volume_size
shape = box
VolumeShape.SPHERE:
var sphere := SphereShape3D.new()
sphere.radius = volume_radius
shape = sphere
VolumeShape.CYLINDER:
var cylinder := CylinderShape3D.new()
cylinder.radius = volume_radius
cylinder.height = volume_size.y
shape = cylinder
_collision_shape.shape = shape
func _on_body_entered(body: Node3D) -> void:
if not enabled:
return
if _is_precipitation_particle(body):
_handle_particle_enter(body)
func _on_body_exited(body: Node3D) -> void:
_handle_particle_exit(body)
func _on_area_entered(area: Area3D) -> void:
if not enabled:
return
if area.has_meta("precipitation_particle"):
_handle_particle_enter(area)
func _on_area_exited(area: Area3D) -> void:
_handle_particle_exit(area)
func _is_precipitation_particle(node: Node3D) -> bool:
if node.has_meta("particle_type"):
var particle_type: String = node.get_meta("particle_type")
if block_all_precipitation:
return particle_type in ["rain", "snow", "hail", "sleet"]
if block_rain and particle_type == "rain":
return true
if block_snow and particle_type == "snow":
return true
return false
func _handle_particle_enter(node: Node3D) -> void:
if instant_kill:
_kill_particle(node)
else:
_particles_inside[node.get_instance_id()] = {
"node": node,
"fade_time": 0.0
}
func _handle_particle_exit(node: Node3D) -> void:
var id := node.get_instance_id()
if _particles_inside.has(id):
_particles_inside.erase(id)
func _kill_particle(node: Node3D) -> void:
if node.has_method("kill"):
node.call("kill")
elif node is GPUParticles3D:
node.emitting = false
elif node is CPUParticles3D:
node.emitting = false
func _process(delta: float) -> void:
if instant_kill or _particles_inside.is_empty():
return
var to_remove: Array[int] = []
for id in _particles_inside:
var data: Dictionary = _particles_inside[id]
var node: Node3D = data["node"]
if not is_instance_valid(node):
to_remove.append(id)
continue
data["fade_time"] += delta
var fade_progress: float = data["fade_time"] / fade_out_duration
if fade_progress >= 1.0:
_kill_particle(node)
to_remove.append(id)
else:
_apply_fade(node, 1.0 - fade_progress)
for id in to_remove:
_particles_inside.erase(id)
func _apply_fade(node: Node3D, alpha: float) -> void:
if node.has_method("set_alpha"):
node.call("set_alpha", alpha)
elif node is GPUParticles3D or node is CPUParticles3D:
pass
func is_point_inside(point: Vector3) -> bool:
var local_point := to_local(point)
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
return abs(local_point.x) <= half_size.x and \
abs(local_point.y) <= half_size.y and \
abs(local_point.z) <= half_size.z
VolumeShape.SPHERE:
return local_point.length() <= volume_radius
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
return horizontal_dist <= volume_radius and abs(local_point.y) <= half_height
return false
func is_point_in_fade_margin(point: Vector3) -> bool:
if fade_out_margin <= 0:
return false
var local_point := to_local(point)
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
var inner_half := half_size - Vector3(fade_out_margin, fade_out_margin, fade_out_margin)
var in_outer := abs(local_point.x) <= half_size.x and \
abs(local_point.y) <= half_size.y and \
abs(local_point.z) <= half_size.z
var in_inner := abs(local_point.x) <= inner_half.x and \
abs(local_point.y) <= inner_half.y and \
abs(local_point.z) <= inner_half.z
return in_outer and not in_inner
VolumeShape.SPHERE:
var dist := local_point.length()
return dist <= volume_radius and dist >= volume_radius - fade_out_margin
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
var in_outer := horizontal_dist <= volume_radius and abs(local_point.y) <= half_height
var in_inner := horizontal_dist <= volume_radius - fade_out_margin and \
abs(local_point.y) <= half_height - fade_out_margin
return in_outer and not in_inner
return false
func get_fade_factor(point: Vector3) -> float:
if not is_point_inside(point):
return 1.0
if not is_point_in_fade_margin(point):
return 0.0
var local_point := to_local(point)
var min_distance_to_edge := fade_out_margin
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
min_distance_to_edge = minf(min_distance_to_edge, half_size.x - abs(local_point.x))
min_distance_to_edge = minf(min_distance_to_edge, half_size.y - abs(local_point.y))
min_distance_to_edge = minf(min_distance_to_edge, half_size.z - abs(local_point.z))
VolumeShape.SPHERE:
min_distance_to_edge = volume_radius - local_point.length()
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
min_distance_to_edge = minf(volume_radius - horizontal_dist, half_height - abs(local_point.y))
return clampf(min_distance_to_edge / fade_out_margin, 0.0, 1.0)

View File

@@ -0,0 +1 @@
uid://burw0wjl85d8a