class_name MeshSurfaceAnchorSampler extends RefCounted const HEIGHT_EPSILON := 0.0001 static func sample_vertical_surface( visual_root: Node3D, relative_root: Node3D, material_names: PackedStringArray, minimum_height: float, maximum_height: float, maximum_up_dot: float, clearance: float, random: RandomNumberGenerator, ) -> Dictionary: if ( visual_root == null or relative_root == null or material_names.is_empty() or maximum_height <= minimum_height or random == null ): return {} var candidates: Array[Dictionary] = [] _collect_candidates( visual_root, relative_root, material_names, minimum_height, maximum_height, clampf(maximum_up_dot, 0.0, 1.0), candidates, ) while not candidates.is_empty(): var candidate_index := _weighted_candidate_index(candidates, random) var candidate: Dictionary = candidates[candidate_index] candidates.remove_at(candidate_index) var point := _sample_triangle_height_slice( candidate["a"], candidate["b"], candidate["c"], minimum_height, maximum_height, random, ) if not point.is_finite(): continue var normal: Vector3 = candidate["normal"] var surface_normal := Vector3(normal.x, 0.0, normal.z).normalized() if surface_normal.is_zero_approx(): continue return { "position": point + surface_normal * maxf(clearance, 0.0), "surface_position": point, "surface_normal": surface_normal, } return {} static func _collect_candidates( node: Node, relative_root: Node3D, material_names: PackedStringArray, minimum_height: float, maximum_height: float, maximum_up_dot: float, candidates: Array[Dictionary], ) -> void: var mesh_instance := node as MeshInstance3D if mesh_instance != null and mesh_instance.mesh != null: _collect_mesh_candidates( mesh_instance, relative_root, material_names, minimum_height, maximum_height, maximum_up_dot, candidates, ) for child: Node in node.get_children(): _collect_candidates( child, relative_root, material_names, minimum_height, maximum_height, maximum_up_dot, candidates, ) static func _collect_mesh_candidates( mesh_instance: MeshInstance3D, relative_root: Node3D, material_names: PackedStringArray, minimum_height: float, maximum_height: float, maximum_up_dot: float, candidates: Array[Dictionary], ) -> void: var mesh := mesh_instance.mesh var to_relative := ( relative_root.global_transform.affine_inverse() * mesh_instance.global_transform ) for surface_index: int in mesh.get_surface_count(): if ( mesh is ArrayMesh and (mesh as ArrayMesh).surface_get_primitive_type(surface_index) != Mesh.PRIMITIVE_TRIANGLES ): continue var material := mesh.surface_get_material(surface_index) if not _material_matches(material, material_names): continue var arrays := mesh.surface_get_arrays(surface_index) var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array if indices.is_empty(): for vertex_index: int in range(0, vertices.size() - 2, 3): _add_triangle_candidate( to_relative * vertices[vertex_index], to_relative * vertices[vertex_index + 1], to_relative * vertices[vertex_index + 2], minimum_height, maximum_height, maximum_up_dot, candidates, ) continue for index_offset: int in range(0, indices.size() - 2, 3): _add_triangle_candidate( to_relative * vertices[indices[index_offset]], to_relative * vertices[indices[index_offset + 1]], to_relative * vertices[indices[index_offset + 2]], minimum_height, maximum_height, maximum_up_dot, candidates, ) static func _material_matches( material: Material, material_names: PackedStringArray, ) -> bool: if material == null: return false var candidate_name := material.resource_name.to_lower() for configured_name: String in material_names: if candidate_name == configured_name.to_lower(): return true return false static func _add_triangle_candidate( a: Vector3, b: Vector3, c: Vector3, minimum_height: float, maximum_height: float, maximum_up_dot: float, candidates: Array[Dictionary], ) -> void: var cross := (b - a).cross(c - a) var doubled_area := cross.length() if doubled_area <= HEIGHT_EPSILON: return var normal := cross / doubled_area if absf(normal.dot(Vector3.UP)) > maximum_up_dot: return var triangle_minimum := minf(a.y, minf(b.y, c.y)) var triangle_maximum := maxf(a.y, maxf(b.y, c.y)) var overlap := ( minf(triangle_maximum, maximum_height) - maxf(triangle_minimum, minimum_height) ) if overlap <= HEIGHT_EPSILON: return candidates.append({ "a": a, "b": b, "c": c, "normal": normal, "weight": doubled_area * 0.5 * overlap, }) static func _weighted_candidate_index( candidates: Array[Dictionary], random: RandomNumberGenerator, ) -> int: var total_weight := 0.0 for candidate: Dictionary in candidates: total_weight += float(candidate.get("weight", 0.0)) if total_weight <= 0.0: return random.randi_range(0, candidates.size() - 1) var roll := random.randf() * total_weight var cumulative := 0.0 for index: int in candidates.size(): cumulative += float(candidates[index].get("weight", 0.0)) if roll <= cumulative: return index return candidates.size() - 1 static func _sample_triangle_height_slice( a: Vector3, b: Vector3, c: Vector3, minimum_height: float, maximum_height: float, random: RandomNumberGenerator, ) -> Vector3: var slice_minimum := maxf(minimum_height, minf(a.y, minf(b.y, c.y))) var slice_maximum := minf(maximum_height, maxf(a.y, maxf(b.y, c.y))) if slice_maximum - slice_minimum <= HEIGHT_EPSILON: return Vector3(INF, INF, INF) var target_height := random.randf_range(slice_minimum, slice_maximum) var intersections := PackedVector3Array() _append_edge_intersection(a, b, target_height, intersections) _append_edge_intersection(b, c, target_height, intersections) _append_edge_intersection(c, a, target_height, intersections) if intersections.size() < 2: return Vector3(INF, INF, INF) var first := intersections[0] var second := intersections[1] var greatest_distance := first.distance_squared_to(second) for first_index: int in intersections.size(): for second_index: int in range(first_index + 1, intersections.size()): var distance := intersections[first_index].distance_squared_to( intersections[second_index] ) if distance > greatest_distance: greatest_distance = distance first = intersections[first_index] second = intersections[second_index] return first.lerp(second, random.randf()) static func _append_edge_intersection( a: Vector3, b: Vector3, height: float, intersections: PackedVector3Array, ) -> void: var minimum := minf(a.y, b.y) var maximum := maxf(a.y, b.y) if height < minimum - HEIGHT_EPSILON or height > maximum + HEIGHT_EPSILON: return var height_delta := b.y - a.y if absf(height_delta) <= HEIGHT_EPSILON: return var weight := clampf((height - a.y) / height_delta, 0.0, 1.0) var point := a.lerp(b, weight) for existing: Vector3 in intersections: if existing.distance_squared_to(point) <= HEIGHT_EPSILON * HEIGHT_EPSILON: return intersections.append(point)