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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue