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: if triangles.is_empty() or total_area <= 0.0:
return Vector3(INF, INF, INF) 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) var fallback: Vector3 = Vector3(INF, INF, INF)
for _attempt: int in attempts: for _attempt: int in attempts:
var roll: float = _rng.randf() * total_area var roll: float = _rng.randf() * total_area
@ -534,12 +539,22 @@ func _sample_surface_position(
+ triangle[2] * barycentric_c + triangle[2] * barycentric_c
) )
fallback = point fallback = point
if ( var within_requested_distance := (
not is_finite(maximum_distance) not is_finite(maximum_distance)
or Vector2(point.x - origin.x, point.z - origin.z).length() or Vector2(point.x - origin.x, point.z - origin.z).length()
<= maximum_distance <= 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 return point
if validates_diggable_surface:
return Vector3(INF, INF, INF)
if origin.is_finite() and is_finite(maximum_distance): if origin.is_finite() and is_finite(maximum_distance):
return origin return origin
return fallback return fallback

View file

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

View file

@ -2,6 +2,10 @@
class_name DiggableArea3D class_name DiggableArea3D
extends Node3D 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 var area_id: StringName
@export_node_path("Node3D") var terrain_source: NodePath @export_node_path("Node3D") var terrain_source: NodePath
@export var surface_materials: Array[StringName] = [] @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(-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 @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]: func get_surface_triangles() -> Array[PackedVector3Array]:
var 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(): if area_id.is_empty() or surface_materials.is_empty():
return triangles return triangles
var terrain_root: Node = get_node_or_null(terrain_source) 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 var mesh: Mesh = mesh_instance.mesh
if mesh == null: if mesh == null:
continue continue
var validation_records: Array[Dictionary] = []
var validation_bounds := Rect2()
var has_validation_bounds := false
for surface_index: int in mesh.get_surface_count(): for surface_index: int in mesh.get_surface_count():
var material: Material = mesh_instance.get_active_material(surface_index) var material: Material = mesh_instance.get_active_material(surface_index)
if ( var allowed_surface := (
material == null material != null
or not surface_materials.has(StringName(material.resource_name)) and surface_materials.has(StringName(material.resource_name))
): )
continue
_append_surface_triangles( _append_surface_triangles(
triangles, triangles,
validation_records,
mesh_instance, mesh_instance,
mesh.surface_get_arrays(surface_index), 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 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( func _append_surface_triangles(
result: Array[PackedVector3Array], result: Array[PackedVector3Array],
validation_records: Array[Dictionary],
mesh_instance: MeshInstance3D, mesh_instance: MeshInstance3D,
arrays: Array, arrays: Array,
allowed_surface: bool,
) -> void: ) -> void:
if arrays.size() <= Mesh.ARRAY_INDEX: if arrays.size() <= Mesh.ARRAY_INDEX:
return return
@ -52,26 +126,49 @@ func _append_surface_triangles(
for vertex_index: int in range(0, vertices.size() - 2, 3): for vertex_index: int in range(0, vertices.size() - 2, 3):
_append_triangle( _append_triangle(
result, result,
validation_records,
mesh_instance.to_global(vertices[vertex_index]), mesh_instance.to_global(vertices[vertex_index]),
mesh_instance.to_global(vertices[vertex_index + 1]), mesh_instance.to_global(vertices[vertex_index + 1]),
mesh_instance.to_global(vertices[vertex_index + 2]), mesh_instance.to_global(vertices[vertex_index + 2]),
allowed_surface,
) )
return return
for index_offset: int in range(0, indices.size() - 2, 3): for index_offset: int in range(0, indices.size() - 2, 3):
_append_triangle( _append_triangle(
result, result,
validation_records,
mesh_instance.to_global(vertices[indices[index_offset]]), 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 + 1]]),
mesh_instance.to_global(vertices[indices[index_offset + 2]]), mesh_instance.to_global(vertices[indices[index_offset + 2]]),
allowed_surface,
) )
func _append_triangle( func _append_triangle(
result: Array[PackedVector3Array], result: Array[PackedVector3Array],
validation_records: Array[Dictionary],
a: Vector3, a: Vector3,
b: Vector3, b: Vector3,
c: Vector3, c: Vector3,
allowed_surface: bool,
) -> void: ) -> 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 ( if (
a.y < minimum_global_y a.y < minimum_global_y
or b.y < minimum_global_y or b.y < minimum_global_y
@ -84,12 +181,56 @@ func _append_triangle(
var center := (a + b + c) / 3.0 var center := (a + b + c) / 3.0
if not generation_bounds.has_point(Vector2(center.x, center.z)): if not generation_bounds.has_point(Vector2(center.x, center.z)):
return return
var cross := (b - a).cross(c - a) if up_dot < minimum_up_dot:
if cross.length_squared() <= 0.0000001:
return return
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot: result.append(triangle)
return
result.append(PackedVector3Array([a, b, c]))
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]: 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: func _on_generation_completed(summary: Dictionary) -> void:
_diggable_beach.invalidate_surface_cache()
var records: Array[Dictionary] = _generator.placement_records() var records: Array[Dictionary] = _generator.placement_records()
_assign_biomes(records, summary) _assign_biomes(records, summary)
var spawn_position := Vector3.ZERO var spawn_position := Vector3.ZERO

View file

@ -207,6 +207,16 @@ func get_diggable_area_triangles(
return [] 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( func get_gatherable_spawn_positions(
anchor_set_id: StringName, anchor_set_id: StringName,
) -> PackedVector3Array: ) -> PackedVector3Array: