@tool
class_name CloudSystem
extends Node3D
## Thin manager for pluggable cloud layers.
## Holds shared state (sun, moon, wind, weather, camera) and delegates to layers.
## Weather control (0.0 = clear cumulus, 1.0 = storm CB)
@export_range(0.0, 1.0) var weather: float = 0.3 :
set(value):
weather = value
_shared.weather = value
# Propagate to spatial plane layer if present
var main_layer := get_spatial_plane_layer()
if main_layer:
main_layer.set_param("weather", value)
## Wind
@export var wind_direction: Vector2 = Vector2(1, 0)
@export var wind_speed: float = 0.02
## Cloud formation type (forwarded to SpatialPlaneLayer)
enum CloudType { CUMULUS, STRATUS, STRATOCUMULUS, CUMULONIMBUS, ALTOCUMULUS, CIRROSTRATUS }
@export var cloud_type: CloudType = CloudType.CUMULUS :
set(value):
cloud_type = value
var main_layer := get_spatial_plane_layer()
if main_layer:
main_layer.apply_cloud_type_preset(value as SpatialPlaneLayer.CloudType)
## Shadow mode (forwarded to ShadowLayer)
enum ShadowMode { ANALYTICAL, DECAL, BOTH, NONE }
@export var shadow_mode: ShadowMode = ShadowMode.DECAL :
set(value):
shadow_mode = value
var shadow := get_shadow_layer()
if shadow:
shadow.set_shadow_mode(value as ShadowLayer.ShadowMode)
@export_range(0.0, 1.0) var shadow_darkness: float = 0.7 :
set(value):
shadow_darkness = value
var shadow := get_shadow_layer()
if shadow:
shadow.set_param("shadow_darkness", value)
## Cloud texture scale
@export_range(0.5, 10.0) var cloud_scale: float = 3.0 :
set(value):
cloud_scale = value
var main_layer := get_spatial_plane_layer()
if main_layer:
main_layer.set_param("cloud_scale", value)
## Cirrus
@export var cirrus_visible: bool = true :
set(value):
cirrus_visible = value
var cirrus := get_cirrus_layer()
if cirrus:
cirrus.enabled = value
@export_range(0.0, 1.0) var cirrus_coverage: float = 0.55 :
set(value):
cirrus_coverage = value
var cirrus := get_cirrus_layer()
if cirrus:
cirrus.set_param("coverage", value)
## Special types
@export var altocumulus_enabled: bool = false
@export var mammatus_enabled: bool = false
## Altitudes
@export var main_cloud_altitude: float = 1500.0
@export var cirrus_altitude: float = 3000.0
## Layer registry
var _layers: Array[CloudLayer] = []
var _shared := CloudSharedState.new()
func _ready() -> void:
# Volumetric clouds now render in the sky shader (SkyMaterial.gdshader).
# CloudSystem only manages the shadow layer for ground shadow projection.
var shadow := ShadowLayer.new()
shadow.shadow_mode = shadow_mode as ShadowLayer.ShadowMode
shadow.shadow_darkness = shadow_darkness
add_layer(shadow)
func _process(delta: float) -> void:
_update_shared_state(delta)
for layer in _layers:
if layer.enabled:
layer.set_visible(true)
layer.update(_shared, delta)
layer.update_lighting(_shared)
else:
layer.set_visible(false)
## Add a cloud layer to the system.
func add_layer(layer: CloudLayer) -> void:
_layers.append(layer)
layer.create(self)
## Remove a cloud layer from the system.
func remove_layer(layer: CloudLayer) -> void:
layer.destroy()
_layers.erase(layer)
## Get all registered layers.
func get_layers() -> Array[CloudLayer]:
return _layers
## Get all layer parameters for debug panel auto-discovery.
func get_all_layer_params() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for layer in _layers:
result.append({
"layer_name": layer.name,
"enabled": layer.enabled,
"params": layer.get_params(),
})
return result
## Find the SpatialPlaneLayer (main cloud layer).
func get_spatial_plane_layer() -> SpatialPlaneLayer:
for layer in _layers:
if layer is SpatialPlaneLayer:
return layer as SpatialPlaneLayer
return null
## Find the CirrusLayer.
func get_cirrus_layer() -> CirrusLayer:
for layer in _layers:
if layer is CirrusLayer:
return layer as CirrusLayer
return null
## Find the ShadowLayer.
func get_shadow_layer() -> ShadowLayer:
for layer in _layers:
if layer is ShadowLayer:
return layer as ShadowLayer
return null
## Get params dict for analytical shadow uniforms on other materials.
func get_cloud_shadow_params() -> Dictionary:
var shadow := get_shadow_layer()
if shadow:
return shadow.get_cloud_shadow_params(_shared.wind_offset, weather)
return {
"cloud_shadow_altitude": main_cloud_altitude,
"cloud_shadow_scale": 1.0 / 10000.0,
"cloud_shadow_wind_offset": _shared.wind_offset,
"cloud_shadow_coverage": lerpf(0.25, 0.95, weather),
"cloud_shadow_darkness": shadow_darkness,
}
## Apply cloud shadow uniforms to a ShaderMaterial.
func apply_cloud_shadow_params(mat: ShaderMaterial) -> void:
var params := get_cloud_shadow_params()
for key in params:
mat.set_shader_parameter(key, params[key])
func _update_shared_state(delta: float) -> void:
_shared.weather = weather
var cam := get_viewport().get_camera_3d()
_shared.camera_pos = cam.global_position if cam else Vector3.ZERO
var sky3d: Sky3DExtended = get_parent() as Sky3DExtended
if sky3d and sky3d.sun:
_shared.sun_direction = -sky3d.sun.global_transform.basis.z
var col: Color = sky3d.sun.light_color
_shared.sun_color = Vector3(col.r, col.g, col.b)
_shared.sun_energy = sky3d.sun.light_energy
if sky3d and sky3d.moon:
_shared.moon_direction = -sky3d.moon.global_transform.basis.z
_shared.moon_energy = sky3d.moon.light_energy
if sky3d and sky3d.tod:
_shared.time_of_day = sky3d.tod.current_time