Anchor beetles to generated tree surfaces
This commit is contained in:
parent
f1c952f5d6
commit
5ed4cccf76
18 changed files with 446 additions and 71 deletions
|
|
@ -10,6 +10,8 @@ catch_data = ExtResource("2_catch")
|
|||
required_tool_id = &"crab_net"
|
||||
spawn_anchor_set_id = &"starter_reachable_tree_trunks"
|
||||
population = 3
|
||||
spawn_anchor_occupancy_ratio = 0.35
|
||||
maximum_anchor_population = 64
|
||||
requires_sneaking = false
|
||||
movement_speed = 0.0
|
||||
roam_radius = 0.1
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ enum PresentationMode {
|
|||
@export var spawn_anchor_set_id: StringName
|
||||
@export_range(-100.0, 100.0, 0.01) var minimum_surface_y: float = 0.08
|
||||
@export_range(0, 64, 1) var population: int = 0
|
||||
## Anchored gatherables can scale with authored/generated attachment geometry.
|
||||
## Zero preserves the fixed population above.
|
||||
@export_range(0.0, 1.0, 0.01) var spawn_anchor_occupancy_ratio := 0.0
|
||||
## Zero leaves the anchor count as the only ceiling.
|
||||
@export_range(0, 256, 1) var maximum_anchor_population := 0
|
||||
@export var presentation_mode: PresentationMode = PresentationMode.VISIBLE_CREATURE
|
||||
@export var requires_sneaking: bool = true
|
||||
@export_range(0.0, 120.0, 0.1) var active_lifetime_seconds: float = 0.0
|
||||
|
|
@ -78,6 +83,10 @@ func is_valid() -> bool:
|
|||
== FishDataType.CollectionMethod.DIGGING
|
||||
)
|
||||
and population > 0
|
||||
and (
|
||||
maximum_anchor_population == 0
|
||||
or maximum_anchor_population >= population
|
||||
)
|
||||
and movement_parameters_valid
|
||||
and _quality_multipliers_are_valid(
|
||||
quality_movement_speed_multipliers
|
||||
|
|
@ -102,6 +111,21 @@ func is_stationary_spawn() -> bool:
|
|||
return is_stationary_hotspot() or not spawn_anchor_set_id.is_empty()
|
||||
|
||||
|
||||
func target_population_for_anchor_count(anchor_count: int) -> int:
|
||||
if spawn_anchor_set_id.is_empty() or spawn_anchor_occupancy_ratio <= 0.0:
|
||||
return population
|
||||
if anchor_count <= 0:
|
||||
return 0
|
||||
var target := maxi(
|
||||
population,
|
||||
ceili(float(anchor_count) * spawn_anchor_occupancy_ratio),
|
||||
)
|
||||
target = mini(target, anchor_count)
|
||||
if maximum_anchor_population > 0:
|
||||
target = mini(target, maximum_anchor_population)
|
||||
return target
|
||||
|
||||
|
||||
func can_be_scared() -> bool:
|
||||
return not is_stationary_spawn() and scare_radius > 0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ func _begin_population_if_ready() -> void:
|
|||
_current_season()
|
||||
):
|
||||
_cache_spawn_surface(entry)
|
||||
for _spawn_index: int in entry.population:
|
||||
for _spawn_index: int in _target_population(entry):
|
||||
_spawn_entity(entry)
|
||||
|
||||
|
||||
|
|
@ -1383,11 +1383,22 @@ func _reconcile_seasonal_population() -> void:
|
|||
for entry: GatherableDataType in _catalog.get_available_entries(season):
|
||||
_cache_spawn_surface(entry)
|
||||
var current_population: int = _population_for_type(entry.type_id)
|
||||
while current_population < entry.population:
|
||||
var target_population := _target_population(entry)
|
||||
while current_population < target_population:
|
||||
_spawn_entity(entry)
|
||||
current_population += 1
|
||||
|
||||
|
||||
func _target_population(entry: GatherableDataType) -> int:
|
||||
if entry == null or entry.spawn_anchor_set_id.is_empty():
|
||||
return entry.population if entry != null else 0
|
||||
var anchors: PackedVector3Array = _spawn_anchor_positions.get(
|
||||
entry.type_id,
|
||||
PackedVector3Array(),
|
||||
)
|
||||
return entry.target_population_for_anchor_count(anchors.size())
|
||||
|
||||
|
||||
func _population_for_type(type_id: StringName) -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _entities.values():
|
||||
|
|
|
|||
|
|
@ -1131,49 +1131,36 @@ func _validate_tree_gatherable_anchors(
|
|||
decorations: Node3D,
|
||||
anchors: GatherableAnchorSet3D,
|
||||
) -> void:
|
||||
var eligible_props: Array[Node3D] = []
|
||||
var eligible_props: Dictionary[StringName, Node3D] = {}
|
||||
for child: Node in decorations.get_children():
|
||||
var prop := child as Node3D
|
||||
if prop == null:
|
||||
continue
|
||||
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
||||
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||
if definition != null and definition.gatherable_anchor_height > 0.0:
|
||||
eligible_props.append(prop)
|
||||
if definition != null and definition.has_gatherable_surface():
|
||||
eligible_props[prop.name] = prop
|
||||
var positions := anchors.get_spawn_positions()
|
||||
assert(positions.size() == eligible_props.size())
|
||||
for prop: Node3D in eligible_props:
|
||||
assert(positions.size() >= 12)
|
||||
for child: Node in anchors.get_children():
|
||||
var anchor := child as Marker3D
|
||||
assert(anchor != null)
|
||||
assert(bool(anchor.get_meta(&"mesh_surface_sampled", false)))
|
||||
var prop_name := StringName(anchor.get_meta(&"terrain_prop_name", &""))
|
||||
assert(eligible_props.has(prop_name))
|
||||
var prop: Node3D = eligible_props[prop_name]
|
||||
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
||||
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||
var visual_scale := float(
|
||||
prop.get_meta(&"terrain_prop_visual_scale", 1.0)
|
||||
)
|
||||
var nearest_anchor := Vector3(INF, INF, INF)
|
||||
var nearest_distance_squared := INF
|
||||
for position: Vector3 in positions:
|
||||
var distance_squared := position.distance_squared_to(
|
||||
prop.global_position
|
||||
)
|
||||
if distance_squared < nearest_distance_squared:
|
||||
nearest_distance_squared = distance_squared
|
||||
nearest_anchor = position
|
||||
assert(nearest_anchor.is_finite())
|
||||
var horizontal_distance := Vector2(
|
||||
nearest_anchor.x - prop.global_position.x,
|
||||
nearest_anchor.z - prop.global_position.z,
|
||||
).length()
|
||||
assert(definition != null and definition.has_gatherable_surface())
|
||||
var local_anchor := prop.to_local(anchor.global_position)
|
||||
assert(
|
||||
horizontal_distance
|
||||
>= definition.gatherable_anchor_surface_radius() * visual_scale
|
||||
local_anchor.y
|
||||
>= definition.gatherable_surface_minimum_height - 0.001
|
||||
)
|
||||
assert(
|
||||
absf(
|
||||
nearest_anchor.y
|
||||
- (
|
||||
prop.global_position.y
|
||||
+ definition.gatherable_anchor_height * visual_scale
|
||||
)
|
||||
) <= 0.001
|
||||
local_anchor.y
|
||||
<= definition.gatherable_surface_maximum_height + 0.001
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ const Gatherables: GatherableCatalog = preload(
|
|||
"res://gathering/catalog/gatherable_catalog.tres"
|
||||
)
|
||||
const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||
const MeshSurfaceAnchorSamplerType = preload(
|
||||
"res://world/generation/mesh_surface_anchor_sampler.gd"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -15,6 +18,7 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_beetle_data()
|
||||
await _validate_mesh_surface_sampler()
|
||||
await _validate_tree_anchors()
|
||||
_validate_anchored_presentation()
|
||||
_validate_three_dimensional_targeting()
|
||||
|
|
@ -32,11 +36,74 @@ func _validate_beetle_data() -> void:
|
|||
assert(beetle.required_tool_id == &"crab_net")
|
||||
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
|
||||
assert(beetle.population == 3)
|
||||
assert(is_equal_approx(beetle.spawn_anchor_occupancy_ratio, 0.35))
|
||||
assert(beetle.maximum_anchor_population == 64)
|
||||
assert(beetle.target_population_for_anchor_count(8) == 3)
|
||||
assert(beetle.target_population_for_anchor_count(40) == 14)
|
||||
assert(beetle.target_population_for_anchor_count(400) == 64)
|
||||
assert(is_equal_approx(beetle.sprite_pixel_size, 0.005))
|
||||
assert(beetle.is_stationary_spawn())
|
||||
assert(not beetle.can_be_scared())
|
||||
|
||||
|
||||
func _validate_mesh_surface_sampler() -> void:
|
||||
var prop := Node3D.new()
|
||||
# The accessibility band is local to the planted prop, so the same tree on
|
||||
# a raised cliff remains reachable from that cliff's walkable surface.
|
||||
prop.position = Vector3(3.0, 12.0, -4.0)
|
||||
root.add_child(prop)
|
||||
var visual := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(2.0, 4.0, 2.0)
|
||||
var wood := StandardMaterial3D.new()
|
||||
wood.resource_name = "wood"
|
||||
box.material = wood
|
||||
visual.mesh = box
|
||||
visual.position.y = 2.0
|
||||
prop.add_child(visual)
|
||||
await process_frame
|
||||
var random := RandomNumberGenerator.new()
|
||||
random.seed = 115
|
||||
var sample: Dictionary = MeshSurfaceAnchorSamplerType.sample_vertical_surface(
|
||||
prop,
|
||||
prop,
|
||||
PackedStringArray(["wood"]),
|
||||
0.7,
|
||||
1.5,
|
||||
0.35,
|
||||
0.025,
|
||||
random,
|
||||
)
|
||||
assert(not sample.is_empty())
|
||||
var surface_position: Vector3 = sample["surface_position"]
|
||||
var anchor_position: Vector3 = sample["position"]
|
||||
assert(surface_position.y >= 0.7 and surface_position.y <= 1.5)
|
||||
var world_anchor_position := prop.to_global(anchor_position)
|
||||
assert(
|
||||
world_anchor_position.y >= 12.7
|
||||
and world_anchor_position.y <= 13.5
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
maxf(absf(surface_position.x), absf(surface_position.z)),
|
||||
1.0,
|
||||
))
|
||||
assert(is_equal_approx(
|
||||
anchor_position.distance_to(surface_position),
|
||||
0.025,
|
||||
))
|
||||
assert(MeshSurfaceAnchorSamplerType.sample_vertical_surface(
|
||||
prop,
|
||||
prop,
|
||||
PackedStringArray(["leaf"]),
|
||||
0.7,
|
||||
1.5,
|
||||
0.35,
|
||||
0.025,
|
||||
random,
|
||||
).is_empty())
|
||||
prop.queue_free()
|
||||
|
||||
|
||||
func _validate_tree_anchors() -> void:
|
||||
var region := StarterIslandScene.instantiate() as WorldRegion
|
||||
root.add_child(region)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ func _validate_catalog_statuses() -> void:
|
|||
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
|
||||
assert(beetle.required_tool_id == &"crab_net")
|
||||
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
|
||||
assert(is_equal_approx(beetle.spawn_anchor_occupancy_ratio, 0.35))
|
||||
assert(beetle.maximum_anchor_population == 64)
|
||||
assert(beetle.is_stationary_spawn())
|
||||
assert(not beetle.is_stationary_hotspot())
|
||||
assert(not beetle.requires_sneaking)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ const FishingShopInteractionType = preload(
|
|||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
const MeshSurfaceAnchorSamplerType = preload(
|
||||
"res://world/generation/mesh_surface_anchor_sampler.gd"
|
||||
)
|
||||
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
||||
const SALT_WATER_MATERIAL: Material = preload(
|
||||
"res://world/materials/stylized_water.tres"
|
||||
|
|
@ -35,7 +38,6 @@ const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
|
|||
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||
const PROP_CHANCE_SCALE := 10000
|
||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||
const GATHERABLE_ANCHOR_SURFACE_CLEARANCE := 0.02
|
||||
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
||||
&"grass_tree",
|
||||
&"grass_detail",
|
||||
|
|
@ -681,25 +683,37 @@ func _instantiate_prop(
|
|||
definition.clearance_radius * visual_scale
|
||||
)
|
||||
_placed_prop_groups.append(definition.procedural_group)
|
||||
if definition.gatherable_anchor_height > 0.0:
|
||||
var anchor := Marker3D.new()
|
||||
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
||||
var local_anchor_position := (
|
||||
definition.collision_offset * visual_scale
|
||||
+ Vector3(
|
||||
0.0,
|
||||
definition.gatherable_anchor_height * visual_scale,
|
||||
-(
|
||||
definition.gatherable_anchor_surface_radius()
|
||||
* visual_scale
|
||||
+ GATHERABLE_ANCHOR_SURFACE_CLEARANCE
|
||||
),
|
||||
if definition.has_gatherable_surface():
|
||||
var surface_sample: Dictionary = (
|
||||
MeshSurfaceAnchorSamplerType.sample_vertical_surface(
|
||||
visual_root,
|
||||
prop,
|
||||
definition.gatherable_surface_material_names,
|
||||
definition.gatherable_surface_minimum_height,
|
||||
definition.gatherable_surface_maximum_height,
|
||||
definition.gatherable_surface_maximum_up_dot,
|
||||
definition.gatherable_surface_clearance,
|
||||
random,
|
||||
)
|
||||
)
|
||||
anchor.position = prop.position + local_anchor_position.rotated(
|
||||
Vector3.UP,
|
||||
yaw,
|
||||
if surface_sample.is_empty():
|
||||
push_warning(
|
||||
"No reachable gatherable mesh surface found on %s."
|
||||
% definition.stable_id
|
||||
)
|
||||
return true
|
||||
var anchor := Marker3D.new()
|
||||
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
||||
var local_anchor_position: Vector3 = surface_sample["position"]
|
||||
anchor.position = _tree_anchors.to_local(
|
||||
prop.to_global(local_anchor_position)
|
||||
)
|
||||
anchor.set_meta(
|
||||
&"terrain_prop_id",
|
||||
definition.stable_id,
|
||||
)
|
||||
anchor.set_meta(&"terrain_prop_name", prop.name)
|
||||
anchor.set_meta(&"mesh_surface_sampled", true)
|
||||
_tree_anchors.add_child(anchor)
|
||||
return true
|
||||
|
||||
|
|
|
|||
260
world/generation/mesh_surface_anchor_sampler.gd
Normal file
260
world/generation/mesh_surface_anchor_sampler.gd
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
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)
|
||||
1
world/generation/mesh_surface_anchor_sampler.gd.uid
Normal file
1
world/generation/mesh_surface_anchor_sampler.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dlen7b42nxpjv
|
||||
|
|
@ -23,3 +23,4 @@ local_overhang_direction = Vector2(-0.883, 0.469)
|
|||
ocean_facing_spread_degrees = 55.0
|
||||
collision_radius = 0.4
|
||||
collision_height = 6.0
|
||||
gatherable_surface_material_names = PackedStringArray("wood_light")
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ minimum_visual_scale = 0.65
|
|||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.5
|
||||
collision_height = 4.0
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood")
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ minimum_visual_scale = 0.75
|
|||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.65
|
||||
collision_height = 9.5
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood")
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light",
|
|||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.4
|
||||
collision_height = 3.2
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark")
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light",
|
|||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.5
|
||||
collision_height = 3.8
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark")
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light",
|
|||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.55
|
||||
collision_height = 4.2
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark")
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light",
|
|||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.85
|
||||
collision_height = 7.5
|
||||
gatherable_anchor_height = 2.15
|
||||
gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark")
|
||||
|
|
|
|||
|
|
@ -74,11 +74,12 @@ func validation_errors() -> PackedStringArray:
|
|||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.gatherable_anchor_height > 0.0
|
||||
and definition.gatherable_anchor_surface_radius() <= 0.0
|
||||
not definition.gatherable_surface_material_names.is_empty()
|
||||
and definition.gatherable_surface_maximum_height
|
||||
<= definition.gatherable_surface_minimum_height
|
||||
):
|
||||
errors.append(
|
||||
"%s is gatherable but has no trunk-surface radius."
|
||||
"%s has an invalid gatherable-surface height band."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -47,11 +47,18 @@ extends Resource
|
|||
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0
|
||||
@export var collision_box_size := Vector3.ZERO
|
||||
@export var collision_offset := Vector3.ZERO
|
||||
## Values above zero add this prop to the tree-gathering anchor set.
|
||||
@export_range(0.0, 20.0, 0.05) var gatherable_anchor_height := 0.0
|
||||
## Optional distance from the prop origin to the visible trunk surface. A
|
||||
## zero value derives the distance from the authored collision shape.
|
||||
@export_range(0.0, 5.0, 0.05) var gatherable_anchor_radius := 0.0
|
||||
@export_category("Gatherable Surface")
|
||||
## Non-empty values explicitly designate this prop's matching mesh surfaces as
|
||||
## valid attachment geometry. Unlisted props and materials are never sampled.
|
||||
@export var gatherable_surface_material_names := PackedStringArray()
|
||||
## Accessibility band measured upward from this prop's planted origin. It
|
||||
## follows the tree onto hills/cliffs while keeping anchors within net reach.
|
||||
@export_range(0.0, 20.0, 0.05) var gatherable_surface_minimum_height := 0.7
|
||||
@export_range(0.0, 20.0, 0.05) var gatherable_surface_maximum_height := 1.5
|
||||
## Reject upward-facing branches and foliage so attachments favor trunk-like
|
||||
## faces. Zero accepts only vertical faces; one accepts every orientation.
|
||||
@export_range(0.0, 1.0, 0.05) var gatherable_surface_maximum_up_dot := 0.35
|
||||
@export_range(0.0, 0.25, 0.005) var gatherable_surface_clearance := 0.025
|
||||
|
||||
|
||||
func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool:
|
||||
|
|
@ -77,14 +84,12 @@ func has_box_collision() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func gatherable_anchor_surface_radius() -> float:
|
||||
if gatherable_anchor_radius > 0.0:
|
||||
return gatherable_anchor_radius
|
||||
if has_cylinder_collision():
|
||||
return collision_radius
|
||||
if has_box_collision():
|
||||
return maxf(collision_box_size.x, collision_box_size.z) * 0.5
|
||||
return 0.0
|
||||
func has_gatherable_surface() -> bool:
|
||||
return (
|
||||
not gatherable_surface_material_names.is_empty()
|
||||
and gatherable_surface_maximum_height
|
||||
> gatherable_surface_minimum_height
|
||||
)
|
||||
|
||||
|
||||
func is_procedural() -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue