Start dynamic sky
This commit is contained in:
301
dynamic-sky/scripts/cloud_system_2d.gd
Normal file
301
dynamic-sky/scripts/cloud_system_2d.gd
Normal 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()
|
||||
1
dynamic-sky/scripts/cloud_system_2d.gd.uid
Normal file
1
dynamic-sky/scripts/cloud_system_2d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cwplvsarljsf
|
||||
313
dynamic-sky/scripts/day_night_controller.gd
Normal file
313
dynamic-sky/scripts/day_night_controller.gd
Normal 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"
|
||||
1
dynamic-sky/scripts/day_night_controller.gd.uid
Normal file
1
dynamic-sky/scripts/day_night_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://gypnoh5ikwjn
|
||||
399
dynamic-sky/scripts/dynamic_sky_root.gd
Normal file
399
dynamic-sky/scripts/dynamic_sky_root.gd
Normal 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
|
||||
1
dynamic-sky/scripts/dynamic_sky_root.gd.uid
Normal file
1
dynamic-sky/scripts/dynamic_sky_root.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8r3do6ow5nmn
|
||||
310
dynamic-sky/scripts/post_process_controller.gd
Normal file
310
dynamic-sky/scripts/post_process_controller.gd
Normal 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)
|
||||
1
dynamic-sky/scripts/post_process_controller.gd.uid
Normal file
1
dynamic-sky/scripts/post_process_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cwqg0bg2m2bme
|
||||
93
dynamic-sky/scripts/procedural_cloud_texture.gd
Normal file
93
dynamic-sky/scripts/procedural_cloud_texture.gd
Normal 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()
|
||||
1
dynamic-sky/scripts/procedural_cloud_texture.gd.uid
Normal file
1
dynamic-sky/scripts/procedural_cloud_texture.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dyjrdeqft0xlk
|
||||
220
dynamic-sky/scripts/shadow_proxy_system.gd
Normal file
220
dynamic-sky/scripts/shadow_proxy_system.gd
Normal 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()
|
||||
1
dynamic-sky/scripts/shadow_proxy_system.gd.uid
Normal file
1
dynamic-sky/scripts/shadow_proxy_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://hvs2g6acqryq
|
||||
171
dynamic-sky/scripts/sky_debug_panel.gd
Normal file
171
dynamic-sky/scripts/sky_debug_panel.gd
Normal 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)
|
||||
1
dynamic-sky/scripts/sky_debug_panel.gd.uid
Normal file
1
dynamic-sky/scripts/sky_debug_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cimhpqq1qo80b
|
||||
131
dynamic-sky/scripts/skybox_preset_library.gd
Normal file
131
dynamic-sky/scripts/skybox_preset_library.gd
Normal 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)
|
||||
1
dynamic-sky/scripts/skybox_preset_library.gd.uid
Normal file
1
dynamic-sky/scripts/skybox_preset_library.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cb8c6ug44jx4t
|
||||
299
dynamic-sky/scripts/vfx_controller.gd
Normal file
299
dynamic-sky/scripts/vfx_controller.gd
Normal 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
|
||||
1
dynamic-sky/scripts/vfx_controller.gd.uid
Normal file
1
dynamic-sky/scripts/vfx_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cnvj3m5fpsdtj
|
||||
278
dynamic-sky/scripts/weather_controller.gd
Normal file
278
dynamic-sky/scripts/weather_controller.gd
Normal 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
|
||||
1
dynamic-sky/scripts/weather_controller.gd.uid
Normal file
1
dynamic-sky/scripts/weather_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://blyx8v2gq8nlw
|
||||
277
dynamic-sky/scripts/weather_kill_volume.gd
Normal file
277
dynamic-sky/scripts/weather_kill_volume.gd
Normal 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)
|
||||
1
dynamic-sky/scripts/weather_kill_volume.gd.uid
Normal file
1
dynamic-sky/scripts/weather_kill_volume.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://burw0wjl85d8a
|
||||
Reference in New Issue
Block a user