fix clam surface spawn validation
This commit is contained in:
parent
135fd9110f
commit
046f5e0fdb
5 changed files with 208 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue