@tool class_name DiggableArea3D extends Node3D const SURFACE_HEIGHT_TOLERANCE := 0.015 const SURFACE_BOUNDS_TOLERANCE := 0.001 const MINIMUM_VALIDATION_UP_DOT := 0.01 @export var area_id: StringName @export_node_path("Node3D") var terrain_source: NodePath @export var surface_materials: Array[StringName] = [] @export var generation_bounds := Rect2(-50.0, -50.0, 100.0, 100.0) @export_range(-100.0, 100.0, 0.01) var minimum_global_y: float = -100.0 @export_range(-100.0, 100.0, 0.01) var maximum_global_y: float = 100.0 @export_range(0.0, 1.0, 0.01) var minimum_up_dot: float = 0.6 var _surface_validation_groups: Array[Dictionary] = [] var _surface_validation_ready := false func get_surface_triangles() -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] _surface_validation_groups.clear() _surface_validation_ready = false if area_id.is_empty() or surface_materials.is_empty(): return triangles var terrain_root: Node = get_node_or_null(terrain_source) if terrain_root == null: return triangles for mesh_instance: MeshInstance3D in _terrain_mesh_instances(terrain_root): var mesh: Mesh = mesh_instance.mesh if mesh == null: continue var validation_records: Array[Dictionary] = [] var validation_bounds := Rect2() var has_validation_bounds := false for surface_index: int in mesh.get_surface_count(): var material: Material = mesh_instance.get_active_material(surface_index) var allowed_surface := ( material != null and surface_materials.has(StringName(material.resource_name)) ) _append_surface_triangles( triangles, validation_records, mesh_instance, mesh.surface_get_arrays(surface_index), allowed_surface, ) for record: Dictionary in validation_records: var record_bounds: Rect2 = record.get("bounds", Rect2()) if not has_validation_bounds: validation_bounds = record_bounds has_validation_bounds = true else: validation_bounds = validation_bounds.merge(record_bounds) if not validation_records.is_empty(): _surface_validation_groups.append({ "bounds": validation_bounds, "records": validation_records, }) _surface_validation_ready = true return triangles func invalidate_surface_cache() -> void: _surface_validation_groups.clear() _surface_validation_ready = false func is_surface_point_valid(point: Vector3) -> bool: if not point.is_finite(): return false if not _surface_validation_ready: get_surface_triangles() var sample := Vector2(point.x, point.z) var highest_y := -INF var highest_is_allowed := false var found_surface := false for group: Dictionary in _surface_validation_groups: var group_bounds: Rect2 = group.get("bounds", Rect2()) if not group_bounds.grow(SURFACE_BOUNDS_TOLERANCE).has_point(sample): continue var records: Array = group.get("records", []) for record: Dictionary in records: var record_bounds: Rect2 = record.get("bounds", Rect2()) if not record_bounds.grow(SURFACE_BOUNDS_TOLERANCE).has_point(sample): continue var triangle := record.get( "triangle", PackedVector3Array() ) as PackedVector3Array var surface_y := _surface_height_at_point(triangle, sample) if not is_finite(surface_y): continue var allowed := bool(record.get("allowed", false)) if surface_y > highest_y + SURFACE_HEIGHT_TOLERANCE: highest_y = surface_y highest_is_allowed = allowed found_surface = true elif absf(surface_y - highest_y) <= SURFACE_HEIGHT_TOLERANCE: # Material seams may expose coplanar grass and sand triangles. # Treat any tied disallowed surface as authoritative so a dig # hotspot can never appear in visually grassy terrain. highest_is_allowed = highest_is_allowed and allowed return ( found_surface and highest_is_allowed and absf(highest_y - point.y) <= SURFACE_HEIGHT_TOLERANCE ) func _append_surface_triangles( result: Array[PackedVector3Array], validation_records: Array[Dictionary], mesh_instance: MeshInstance3D, arrays: Array, allowed_surface: bool, ) -> void: if arrays.size() <= Mesh.ARRAY_INDEX: return var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array if vertices.is_empty(): return if indices.is_empty(): for vertex_index: int in range(0, vertices.size() - 2, 3): _append_triangle( result, validation_records, mesh_instance.to_global(vertices[vertex_index]), mesh_instance.to_global(vertices[vertex_index + 1]), mesh_instance.to_global(vertices[vertex_index + 2]), allowed_surface, ) return for index_offset: int in range(0, indices.size() - 2, 3): _append_triangle( result, validation_records, mesh_instance.to_global(vertices[indices[index_offset]]), mesh_instance.to_global(vertices[indices[index_offset + 1]]), mesh_instance.to_global(vertices[indices[index_offset + 2]]), allowed_surface, ) func _append_triangle( result: Array[PackedVector3Array], validation_records: Array[Dictionary], a: Vector3, b: Vector3, c: Vector3, allowed_surface: bool, ) -> void: var cross := (b - a).cross(c - a) if cross.length_squared() <= 0.0000001: return var up_dot := absf(cross.normalized().dot(Vector3.UP)) var triangle := PackedVector3Array([a, b, c]) var projected_bounds := _projected_bounds(triangle) if ( up_dot >= MINIMUM_VALIDATION_UP_DOT and generation_bounds.intersects(projected_bounds, true) ): validation_records.append({ "allowed": allowed_surface, "bounds": projected_bounds, "triangle": triangle, }) if not allowed_surface: return if ( a.y < minimum_global_y or b.y < minimum_global_y or c.y < minimum_global_y or a.y > maximum_global_y or b.y > maximum_global_y or c.y > maximum_global_y ): return var center := (a + b + c) / 3.0 if not generation_bounds.has_point(Vector2(center.x, center.z)): return if up_dot < minimum_up_dot: return result.append(triangle) static func _projected_bounds(triangle: PackedVector3Array) -> Rect2: var minimum := Vector2(triangle[0].x, triangle[0].z) var maximum := minimum for index: int in range(1, triangle.size()): var point := Vector2(triangle[index].x, triangle[index].z) minimum = minimum.min(point) maximum = maximum.max(point) return Rect2(minimum, maximum - minimum) static func _surface_height_at_point( triangle: PackedVector3Array, point: Vector2, ) -> float: if triangle.size() != 3: return INF var a := Vector2(triangle[0].x, triangle[0].z) var b := Vector2(triangle[1].x, triangle[1].z) var c := Vector2(triangle[2].x, triangle[2].z) var denominator := ( (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y) ) if absf(denominator) <= 0.0000001: return INF var weight_a := ( (b.y - c.y) * (point.x - c.x) + (c.x - b.x) * (point.y - c.y) ) / denominator var weight_b := ( (c.y - a.y) * (point.x - c.x) + (a.x - c.x) * (point.y - c.y) ) / denominator var weight_c := 1.0 - weight_a - weight_b if ( weight_a < -SURFACE_BOUNDS_TOLERANCE or weight_b < -SURFACE_BOUNDS_TOLERANCE or weight_c < -SURFACE_BOUNDS_TOLERANCE ): return INF return ( triangle[0].y * weight_a + triangle[1].y * weight_b + triangle[2].y * weight_c ) func _terrain_mesh_instances(terrain_root: Node) -> Array[MeshInstance3D]: # Generated terrain can contain authored base layers beneath raised visual # overlays. Those meshes are useful for closing terrain seams, but they are # not necessarily the visible surface and must not produce buried dig spots. # A terrain provider may therefore expose its authoritative primary meshes. if terrain_root.has_method(&"get_primary_terrain_meshes"): var provided: Variant = terrain_root.call(&"get_primary_terrain_meshes") var meshes: Array[MeshInstance3D] = [] if provided is Array: for value: Variant in provided: var mesh_instance := value as MeshInstance3D if mesh_instance != null: meshes.append(mesh_instance) return meshes return _collect_mesh_instances(terrain_root) func _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]: var meshes: Array[MeshInstance3D] = [] if root is MeshInstance3D: meshes.append(root as MeshInstance3D) for child: Node in root.get_children(): meshes.append_array(_collect_mesh_instances(child)) return meshes func _get_configuration_warnings() -> PackedStringArray: var warnings := PackedStringArray() if area_id.is_empty(): warnings.append("Diggable area ID is required.") if get_node_or_null(terrain_source) == null: warnings.append("Diggable area terrain source is unavailable.") if surface_materials.is_empty(): warnings.append("At least one terrain material is required.") if maximum_global_y < minimum_global_y: warnings.append("Maximum height must not be below minimum height.") return warnings