## Main volumetric cloud plane layer with fBM noise, light marching, and weather presets.
class_name SpatialPlaneLayer
extends CloudLayer
const CLOUD_MAIN_SHADER = preload("res://addons/sky3d_extended/shaders/cloud_main.gdshader")
var _mesh: MeshInstance3D
var _material: ShaderMaterial
## Cloud formation type
enum CloudType { CUMULUS, STRATUS, STRATOCUMULUS, CUMULONIMBUS, ALTOCUMULUS, CIRROSTRATUS }
var cloud_type: CloudType = CloudType.CUMULUS
## Exported params
var altitude: float = 1500.0
var weather_val: float = 0.3
var cloud_scale: float = 3.0
var bottom_softness: float = 0.35
var top_softness: float = 0.08
var coverage: float = 0.35
var octaves: int = 5
var persistence: float = 0.5
var domain_warp: float = 0.3
var worley_blend: float = 0.35
var light_steps: int = 4
var shadow_density: float = 1.8
var flow_strength: float = 0.25
var flow_scale: float = 1.5
var parallax_depth: float = 0.0
var parallax_layers: int = 12
var altocumulus_enabled: bool = false
var mammatus_enabled: bool = false
func _init() -> void:
name = "Main Clouds"
func get_params() -> Array[Dictionary]:
return [
{"name": "weather", "min": 0.0, "max": 1.0, "step": 0.01, "value": weather_val},
{"name": "coverage", "min": 0.0, "max": 1.0, "step": 0.01, "value": coverage},
{"name": "cloud_scale", "min": 0.5, "max": 10.0, "step": 0.1, "value": cloud_scale},
{"name": "bottom_softness", "min": 0.01, "max": 1.0, "step": 0.01, "value": bottom_softness},
{"name": "top_softness", "min": 0.01, "max": 0.5, "step": 0.01, "value": top_softness},
{"name": "octaves", "min": 3.0, "max": 6.0, "step": 1.0, "value": float(octaves)},
{"name": "persistence", "min": 0.2, "max": 0.6, "step": 0.01, "value": persistence},
{"name": "domain_warp", "min": 0.0, "max": 1.0, "step": 0.01, "value": domain_warp},
{"name": "worley_blend", "min": 0.0, "max": 1.0, "step": 0.01, "value": worley_blend},
{"name": "light_steps", "min": 2.0, "max": 8.0, "step": 1.0, "value": float(light_steps)},
{"name": "shadow_density", "min": 0.5, "max": 4.0, "step": 0.1, "value": shadow_density},
{"name": "flow_strength", "min": 0.0, "max": 1.0, "step": 0.01, "value": flow_strength},
{"name": "flow_scale", "min": 0.1, "max": 5.0, "step": 0.1, "value": flow_scale},
{"name": "parallax_depth", "min": 0.0, "max": 0.3, "step": 0.01, "value": parallax_depth},
{"name": "parallax_layers", "min": 4.0, "max": 24.0, "step": 1.0, "value": float(parallax_layers)},
]
func set_param(param_name: String, value: float) -> void:
match param_name:
"weather":
weather_val = value
_update_weather_params()
"coverage":
coverage = value
if _material:
_material.set_shader_parameter("coverage", value)
"cloud_scale":
cloud_scale = value
if _material:
_material.set_shader_parameter("cloud_scale", value)
"bottom_softness":
bottom_softness = value
if _material:
_material.set_shader_parameter("bottom_softness", value)
"top_softness":
top_softness = value
if _material:
_material.set_shader_parameter("top_softness", value)
"octaves":
octaves = int(value)
if _material:
_material.set_shader_parameter("octaves", octaves)
"persistence":
persistence = value
if _material:
_material.set_shader_parameter("persistence", value)
"domain_warp":
domain_warp = value
if _material:
_material.set_shader_parameter("domain_warp", value)
"worley_blend":
worley_blend = value
if _material:
_material.set_shader_parameter("worley_blend", value)
"light_steps":
light_steps = int(value)
if _material:
_material.set_shader_parameter("light_steps", light_steps)
"shadow_density":
shadow_density = value
if _material:
_material.set_shader_parameter("shadow_density", value)
"flow_strength":
flow_strength = value
if _material:
_material.set_shader_parameter("flow_strength", value)
"flow_scale":
flow_scale = value
if _material:
_material.set_shader_parameter("flow_scale", value)
"parallax_depth":
parallax_depth = value
if _material:
_material.set_shader_parameter("parallax_enabled", value > 0.0)
_material.set_shader_parameter("parallax_depth", value)
"parallax_layers":
parallax_layers = int(value)
if _material:
_material.set_shader_parameter("parallax_layers", parallax_layers)
func get_param(param_name: String) -> float:
match param_name:
"weather": return weather_val
"coverage": return coverage
"cloud_scale": return cloud_scale
"bottom_softness": return bottom_softness
"top_softness": return top_softness
"octaves": return float(octaves)
"persistence": return persistence
"domain_warp": return domain_warp
"worley_blend": return worley_blend
"light_steps": return float(light_steps)
"shadow_density": return shadow_density
"flow_strength": return flow_strength
"flow_scale": return flow_scale
"parallax_depth": return parallax_depth
"parallax_layers": return float(parallax_layers)
return 0.0
func create(parent: Node3D) -> void:
_mesh = MeshInstance3D.new()
_mesh.name = "CloudLayer_Main"
var plane := PlaneMesh.new()
plane.size = Vector2(100000, 100000)
plane.subdivide_width = 128
plane.subdivide_depth = 128
_mesh.mesh = plane
_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_material = ShaderMaterial.new()
_material.shader = CLOUD_MAIN_SHADER
_material.render_priority = -1
_mesh.material_override = _material
_mesh.position.y = altitude
parent.add_child(_mesh)
_material.set_shader_parameter("flow_strength", flow_strength)
_material.set_shader_parameter("flow_scale", flow_scale)
_update_weather_params()
func destroy() -> void:
if is_instance_valid(_mesh):
_mesh.queue_free()
_mesh = null
_material = null
func update(shared: CloudSharedState, delta: float) -> void:
if not _mesh:
return
# Follow camera
_mesh.global_position = Vector3(shared.camera_pos.x, altitude, shared.camera_pos.z)
# Wind
if _material:
_material.set_shader_parameter("wind_offset", shared.wind_offset)
func update_lighting(shared: CloudSharedState) -> void:
if not _material:
return
_material.set_shader_parameter("sun_direction", shared.sun_direction)
_material.set_shader_parameter("sun_color", shared.sun_color)
_material.set_shader_parameter("sun_energy", shared.sun_energy)
_material.set_shader_parameter("moon_direction", shared.moon_direction)
_material.set_shader_parameter("moon_energy", shared.moon_energy)
func set_visible(value: bool) -> void:
if _mesh:
_mesh.visible = value
## Apply weather-driven noise parameters
func _update_weather_params() -> void:
if not _material:
return
_material.set_shader_parameter("cloud_scale", cloud_scale)
_material.set_shader_parameter("weather", weather_val)
_material.set_shader_parameter("coverage", lerpf(0.25, 0.95, weather_val))
_material.set_shader_parameter("bottom_softness", lerpf(0.35, 0.15, weather_val))
_material.set_shader_parameter("top_softness", lerpf(0.08, 0.03, weather_val))
_material.set_shader_parameter("domain_warp", lerpf(0.2, 0.7, weather_val))
_material.set_shader_parameter("worley_blend", lerpf(0.35, 0.5, weather_val))
# Height gradient interpolation: cumulus -> stratocumulus -> cumulonimbus
var cumulus := Vector4(0.01, 0.0625, 0.78, 1.0)
var stratocumulus := Vector4(0.02, 0.20, 0.48, 0.625)
var cumulonimbus := Vector4(0.0, 0.04, 0.90, 1.0)
var grad: Vector4
if weather_val < 0.5:
var t: float = weather_val * 2.0
grad = cumulus.lerp(stratocumulus, t)
else:
var t: float = (weather_val - 0.5) * 2.0
grad = stratocumulus.lerp(cumulonimbus, t)
_material.set_shader_parameter("height_gradient", grad)
_material.set_shader_parameter("altocumulus_enabled", altocumulus_enabled)
_material.set_shader_parameter("mammatus_enabled", mammatus_enabled and weather_val > 0.8)
# Sync local tracking
coverage = lerpf(0.25, 0.95, weather_val)
bottom_softness = lerpf(0.35, 0.15, weather_val)
top_softness = lerpf(0.08, 0.03, weather_val)
domain_warp = lerpf(0.2, 0.7, weather_val)
worley_blend = lerpf(0.35, 0.5, weather_val)
## Apply a cloud formation preset — sets shader params AND syncs local tracking vars.
func apply_cloud_type_preset(type: CloudType) -> void:
cloud_type = type
if not _material:
return
altocumulus_enabled = false
mammatus_enabled = false
_material.set_shader_parameter("lenticular_enabled", false)
match type:
CloudType.CUMULUS:
_apply_preset(0.3, 0.35, 0.15, 0.3, 0.35, 5, 0.5,
Vector4(0.01, 0.0625, 0.78, 1.0))
CloudType.STRATUS:
_apply_preset(0.4, 0.7, 0.3, 0.15, 0.1, 4, 0.4,
Vector4(0.05, 0.25, 0.35, 0.5))
CloudType.STRATOCUMULUS:
_apply_preset(0.5, 0.55, 0.1, 0.4, 0.45, 5, 0.5,
Vector4(0.02, 0.20, 0.48, 0.625))
CloudType.CUMULONIMBUS:
_apply_preset(0.9, 0.85, 0.05, 0.7, 0.5, 6, 0.55,
Vector4(0.0, 0.04, 0.90, 1.0))
mammatus_enabled = true
_material.set_shader_parameter("mammatus_enabled", true)
CloudType.ALTOCUMULUS:
_apply_preset(0.3, 0.3, 0.12, 0.2, 0.6, 4, 0.45,
Vector4(0.05, 0.15, 0.6, 0.8))
altocumulus_enabled = true
_material.set_shader_parameter("altocumulus_enabled", true)
CloudType.CIRROSTRATUS:
_apply_preset(0.2, 0.5, 0.25, 0.1, 0.15, 3, 0.35,
Vector4(0.1, 0.3, 0.5, 0.7))
_material.set_shader_parameter("cloud_scale", cloud_scale)
## Internal: apply preset values to both shader and local tracking vars.
## p_edge is split into bottom (soft fade) and top (crisp edge) softness.
func _apply_preset(p_weather: float, p_coverage: float, p_edge: float,
p_warp: float, p_worley: float, p_octaves: int, p_persist: float,
p_height_grad: Vector4) -> void:
weather_val = p_weather
coverage = p_coverage
bottom_softness = p_edge * 2.3
top_softness = p_edge * 0.55
domain_warp = p_warp
worley_blend = p_worley
octaves = p_octaves
persistence = p_persist
_material.set_shader_parameter("weather", p_weather)
_material.set_shader_parameter("coverage", p_coverage)
_material.set_shader_parameter("bottom_softness", bottom_softness)
_material.set_shader_parameter("top_softness", top_softness)
_material.set_shader_parameter("domain_warp", p_warp)
_material.set_shader_parameter("worley_blend", p_worley)
_material.set_shader_parameter("octaves", p_octaves)
_material.set_shader_parameter("persistence", p_persist)
_material.set_shader_parameter("height_gradient", p_height_grad)
## Get the main cloud shader material (for cloud shadow param distribution)
func get_material() -> ShaderMaterial:
return _material