260 lines
7.1 KiB
GDScript
260 lines
7.1 KiB
GDScript
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)
|