OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / addons / sky3d_extended / src / cloud_layers / spatial_plane_layer.gd

295 lines · gd ref:
## 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