## Procedural snow cap geometry for flat roofs and vehicles. extends Node3D enum PlacementMode { AUTO_TARGET_AABB, MANUAL_SURFACE, } const SNOW_CAP_SHADER: Shader = preload("res://core/daynight/snow_cap.gdshader") @export var placement_mode: PlacementMode = PlacementMode.AUTO_TARGET_AABB @export var target_path: NodePath @export var size_inset: Vector2 = Vector2(0.25, 1.0) @export var position_offset: Vector3 = Vector3(0.0, -0.08, 0.0) @export var max_snow_height: float = 0.2 @export_range(1, 24) var subdivide_width: int = 6 @export_range(1, 24) var subdivide_depth: int = 18 @export var manual_surface_center: Vector3 = Vector3.ZERO @export var manual_surface_size: Vector2 = Vector2(2.0, 6.0) var _mesh_instance: MeshInstance3D func _ready() -> void: add_to_group("weather_overlay_ignore") _ensure_mesh_instance() _rebuild() func rebuild() -> void: _rebuild() func _ensure_mesh_instance() -> void: _mesh_instance = get_node_or_null("SnowCapMesh") as MeshInstance3D if _mesh_instance != null: return _mesh_instance = MeshInstance3D.new() _mesh_instance.name = "SnowCapMesh" _mesh_instance.add_to_group("weather_overlay_ignore") add_child(_mesh_instance) func _rebuild() -> void: var cap_width: float = 0.0 var cap_depth: float = 0.0 var cap_surface_center := Vector3.ZERO if placement_mode == PlacementMode.MANUAL_SURFACE: cap_width = manual_surface_size.x cap_depth = manual_surface_size.y cap_surface_center = manual_surface_center else: var target_mesh: MeshInstance3D = get_node_or_null(target_path) as MeshInstance3D if target_mesh == null: if _mesh_instance: _mesh_instance.visible = false return var target_aabb: AABB = _get_target_aabb(target_mesh) cap_width = max(target_aabb.size.x - size_inset.x, 0.1) cap_depth = max(target_aabb.size.z - size_inset.y, 0.1) cap_surface_center = Vector3( target_aabb.position.x + target_aabb.size.x * 0.5, target_aabb.position.y + target_aabb.size.y, target_aabb.position.z + target_aabb.size.z * 0.5 ) var cap_center := cap_surface_center + Vector3(0.0, max_snow_height * 0.5, 0.0) var box_mesh := BoxMesh.new() box_mesh.size = Vector3(cap_width, max_snow_height, cap_depth) box_mesh.subdivide_width = subdivide_width box_mesh.subdivide_depth = subdivide_depth _mesh_instance.mesh = box_mesh _mesh_instance.position = cap_center + position_offset _mesh_instance.material_override = _build_material(cap_width, cap_depth) _mesh_instance.visible = true func _build_material(cap_width: float, cap_depth: float) -> ShaderMaterial: var material := ShaderMaterial.new() material.shader = SNOW_CAP_SHADER material.set_shader_parameter("max_height", max_snow_height) material.set_shader_parameter("half_width", cap_width * 0.5) material.set_shader_parameter("half_depth", cap_depth * 0.5) return material func _get_target_aabb(target_mesh: MeshInstance3D) -> AABB: var points: Array[Vector3] = [] for point in _get_aabb_points(target_mesh.get_aabb()): points.append(target_mesh.transform * point) var merged := AABB(points[0], Vector3.ZERO) for point in points: merged = merged.expand(point) return merged func _get_aabb_points(aabb: AABB) -> Array[Vector3]: var p: Vector3 = aabb.position var s: Vector3 = aabb.size return [ p, p + Vector3(s.x, 0.0, 0.0), p + Vector3(0.0, s.y, 0.0), p + Vector3(0.0, 0.0, s.z), p + Vector3(s.x, s.y, 0.0), p + Vector3(s.x, 0.0, s.z), p + Vector3(0.0, s.y, s.z), p + s, ]