straywild/world/generation/mesh_surface_anchor_sampler.gd

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)