fix clam surface spawn validation

This commit is contained in:
Alexander Sellite 2026-08-25 02:05:52 -04:00
parent 135fd9110f
commit 046f5e0fdb
5 changed files with 208 additions and 27 deletions

View file

@ -518,7 +518,12 @@ func _sample_surface_position(
)
if triangles.is_empty() or total_area <= 0.0:
return Vector3(INF, INF, INF)
var attempts: int = 48 if is_finite(maximum_distance) else 1
var validates_diggable_surface := not entry.diggable_area_id.is_empty()
var attempts: int = (
48
if validates_diggable_surface or is_finite(maximum_distance)
else 1
)
var fallback: Vector3 = Vector3(INF, INF, INF)
for _attempt: int in attempts:
var roll: float = _rng.randf() * total_area
@ -534,12 +539,22 @@ func _sample_surface_position(
+ triangle[2] * barycentric_c
)
fallback = point
if (
var within_requested_distance := (
not is_finite(maximum_distance)
or Vector2(point.x - origin.x, point.z - origin.z).length()
<= maximum_distance
):
)
var valid_diggable_surface := (
not validates_diggable_surface
or _world.is_diggable_surface_point_valid(
entry.diggable_area_id,
point,
)
)
if within_requested_distance and valid_diggable_surface:
return point
if validates_diggable_surface:
return Vector3(INF, INF, INF)
if origin.is_finite() and is_finite(maximum_distance):
return origin
return fallback

View file

@ -14,11 +14,11 @@ const Gatherables: GatherableCatalog = preload(
class PrimaryTerrainProvider:
extends Node3D
var primary_mesh: MeshInstance3D
var primary_meshes: Array[MeshInstance3D] = []
func get_primary_terrain_meshes() -> Array[MeshInstance3D]:
return [primary_mesh] if primary_mesh != null else []
return primary_meshes
func _initialize() -> void:
@ -98,9 +98,17 @@ func _validate_primary_terrain_provider() -> void:
var provider := PrimaryTerrainProvider.new()
provider.name = "Provider"
fixture.add_child(provider)
provider.primary_mesh = _flat_sand_triangle(0.0)
provider.add_child(provider.primary_mesh)
var buried_base := _flat_sand_triangle(10.0)
var clear_sand := _flat_surface_triangle(0.0, 0.0, &"sand")
var covered_sand := _flat_surface_triangle(10.0, 0.0, &"sand")
var covering_grass := _flat_surface_triangle(10.0, 0.01, &"grass_lite")
for mesh_instance: MeshInstance3D in [
clear_sand,
covered_sand,
covering_grass,
]:
provider.add_child(mesh_instance)
provider.primary_meshes.append(mesh_instance)
var buried_base := _flat_surface_triangle(20.0, 0.0, &"sand")
buried_base.name = "BuriedSandBase"
provider.add_child(buried_base)
var area := DiggableArea3D.new()
@ -111,28 +119,34 @@ func _validate_primary_terrain_provider() -> void:
area.generation_bounds = Rect2(-20.0, -20.0, 40.0, 40.0)
fixture.add_child(area)
var triangles := area.get_surface_triangles()
assert(triangles.size() == 1)
var center := (triangles[0][0] + triangles[0][1] + triangles[0][2]) / 3.0
assert(center.x < 2.0)
assert(triangles.size() == 2)
var clear_center := Vector3(1.0 / 3.0, 0.0, 1.0 / 3.0)
var covered_center := Vector3(10.0 + 1.0 / 3.0, 0.0, 1.0 / 3.0)
assert(area.is_surface_point_valid(clear_center))
assert(not area.is_surface_point_valid(covered_center))
fixture.free()
func _flat_sand_triangle(x_offset: float) -> MeshInstance3D:
func _flat_surface_triangle(
x_offset: float,
height: float,
material_name: StringName,
) -> MeshInstance3D:
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
Vector3(x_offset, 0.0, 0.0),
Vector3(x_offset, 0.0, 1.0),
Vector3(x_offset + 1.0, 0.0, 0.0),
Vector3(x_offset, height, 0.0),
Vector3(x_offset, height, 1.0),
Vector3(x_offset + 1.0, height, 0.0),
])
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
var material := StandardMaterial3D.new()
material.resource_name = "sand"
material.resource_name = str(material_name)
mesh.surface_set_material(0, material)
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = "PrimarySand"
mesh_instance.name = "Primary%s" % str(material_name).capitalize()
mesh_instance.mesh = mesh
return mesh_instance

View file

@ -2,6 +2,10 @@
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] = []
@ -10,9 +14,14 @@ extends Node3D
@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)
@ -22,25 +31,90 @@ func get_surface_triangles() -> Array[PackedVector3Array]:
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)
if (
material == null
or not surface_materials.has(StringName(material.resource_name))
):
continue
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
@ -52,26 +126,49 @@ func _append_surface_triangles(
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
@ -84,12 +181,56 @@ func _append_triangle(
var center := (a + b + c) / 3.0
if not generation_bounds.has_point(Vector2(center.x, center.z)):
return
var cross := (b - a).cross(c - a)
if cross.length_squared() <= 0.0000001:
if up_dot < minimum_up_dot:
return
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
return
result.append(PackedVector3Array([a, b, c]))
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]:

View file

@ -191,6 +191,7 @@ func get_spawn_surface_triangles(
func _on_generation_completed(summary: Dictionary) -> void:
_diggable_beach.invalidate_surface_cache()
var records: Array[Dictionary] = _generator.placement_records()
_assign_biomes(records, summary)
var spawn_position := Vector3.ZERO

View file

@ -207,6 +207,16 @@ func get_diggable_area_triangles(
return []
func is_diggable_surface_point_valid(
area_id: StringName,
point: Vector3,
) -> bool:
if _active_region == null:
return false
var area: DiggableArea3D = _active_region.get_diggable_area(area_id)
return area != null and area.is_surface_point_valid(point)
func get_gatherable_spawn_positions(
anchor_set_id: StringName,
) -> PackedVector3Array: