straywild/world/generation/generated_world_region.gd
Voyager 4575830857 Improve nearby water recovery and camera prompts
Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
2026-08-29 17:43:11 -04:00

1681 lines
48 KiB
GDScript

class_name GeneratedWorldRegion
extends WorldRegion
signal world_generated(seed: int, summary: Dictionary)
const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd"
)
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
const SALT_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water.tres"
)
const FRESH_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water_fresh.tres"
)
const GENERATED_TERRAIN_DIRT_MATERIAL: Material = preload(
"res://world/materials/generated_terrain_dirt.tres"
)
const GENERATED_POND_POOL: FishPool = preload(
"res://fish/pools/generated_pond_pool.tres"
)
const GENERATED_LAKE_POOL: FishPool = preload(
"res://fish/pools/generated_lake_pool.tres"
)
const GENERATED_RIVER_POOL: FishPool = preload(
"res://fish/pools/generated_river_pool.tres"
)
const OCEAN_POOL: FishPool = preload(
"res://fish/pools/starter_ocean_pool.tres"
)
const WATER_HEIGHT := -0.25
const GENERATED_FRESH_WATER_BED_HEIGHT := -2.85
const PROP_EDGE_MARGIN := 2.2
const PROP_PLACEMENT_ATTEMPTS := 12
const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35
const PROP_CHANCE_SCALE := 10000
const PROP_SELECTION_WEIGHT_SCALE := 1000
const WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE := 0.05
const WATER_RECOVERY_MINIMUM_UP_DOT := 0.6
const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6
const WATER_RECOVERY_INSET_STEP := 0.25
const WATER_RECOVERY_MAXIMUM_INSET := 1.5
const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0]
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
Vector2(1.0, 0.0),
Vector2(0.70710678, 0.70710678),
Vector2(0.0, 1.0),
Vector2(-0.70710678, 0.70710678),
Vector2(-1.0, 0.0),
Vector2(-0.70710678, -0.70710678),
Vector2(0.0, -1.0),
Vector2(0.70710678, -0.70710678),
]
const GRASS_PROP_SURFACE_MATERIALS: Array[StringName] = [&"grass_lite"]
const SAND_PROP_SURFACE_MATERIALS: Array[StringName] = [&"sand"]
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
&"grass_tree",
&"grass_detail",
&"sand_tree",
]
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
@export var prop_catalog: TerrainPropCatalog
@export var biome_catalog: TerrainBiomeCatalog
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
@onready var _player_spawn: Marker3D = %PlayerSpawn
@onready var _safe_spawn: SafeRespawnPoint = %SafeSpawn
@onready var _fishing_shop: FishingShopInteractionType = (
$Interactables/FishingShopWorld/InteractionArea
)
@onready var _player_storage: PlayerStorageInteractionType = (
$Interactables/PlayerStorageBox/InteractionArea
)
@onready var _shop_root: Node3D = %FishingShopWorld
@onready var _storage_root: Node3D = %PlayerStorageBox
@onready var _decorations: Node3D = %Decorations
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
@onready var _ocean: WaterBodyAuthoring = %OceanWater
@onready var _fresh_water_root: Node3D = %FreshWaterBodies
@onready var _shoreline_reference: MeshInstance3D = %ShorelineReference
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
var _light_performance_profile := false
var _placed_prop_positions: Array[Vector3] = []
var _placed_prop_clearance_radii: Array[float] = []
var _placed_prop_groups: Array[StringName] = []
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
func _ready() -> void:
_generator.generation_completed.connect(_on_generation_completed)
_validate_prop_catalog()
_validate_biome_catalog()
_configure_static_water()
_build_shoreline_reference()
func generate_world(seed: int) -> bool:
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
return false
if (
seed == _current_seed
and _generator.get_generated_chunks_root() != null
):
return true
_current_seed = seed
_generator.generation_seed = seed
return _generator.generate()
func get_generation_seed() -> int:
return _current_seed
func is_world_generated() -> bool:
return is_instance_valid(_generator.get_generated_chunks_root())
func get_playable_half_extents() -> Vector2:
var size := Vector2(
float(_generator.grid_size.x),
float(_generator.grid_size.y),
) * _generator.catalog.chunk_size
return size * 0.5
func get_player_spawn_transform() -> Transform3D:
return _player_spawn.global_transform
func get_fishing_shop() -> FishingShopInteractionType:
return _fishing_shop
func get_player_storage() -> PlayerStorageInteractionType:
return _player_storage
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return _shoreline_reference
func get_prop_catalog() -> TerrainPropCatalog:
return prop_catalog
func get_biome_catalog() -> TerrainBiomeCatalog:
return biome_catalog
func get_biome_at(coordinate: Vector2i) -> StringName:
return _biome_assignments.get(coordinate, &"")
func set_light_performance_profile(enabled: bool) -> void:
_light_performance_profile = enabled
_apply_water_materials()
func get_spawn_surface_triangles(
material_names: Array[StringName],
minimum_global_y: float,
minimum_up_dot: float = 0.6,
) -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
if material_names.is_empty():
return triangles
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
var mesh: Mesh = mesh_instance.mesh
if mesh == null:
continue
for surface_index: int in mesh.get_surface_count():
var material: Material = mesh_instance.get_active_material(surface_index)
if material == null or not material_names.has(
StringName(material.resource_name)
):
continue
_append_surface_triangles(
triangles,
mesh_instance,
mesh.surface_get_arrays(surface_index),
minimum_global_y,
minimum_up_dot,
)
return triangles
func get_water_recovery_position(
entry_position: Vector3,
fallback_position: Vector3,
) -> Vector3:
if (
not entry_position.is_finite()
or _water_recovery_triangles_by_coordinate.is_empty()
):
return super(entry_position, fallback_position)
var nearest_surface_distance := _nearest_recovery_surface_distance(
entry_position
)
if not is_finite(nearest_surface_distance):
return super(entry_position, fallback_position)
for expansion: float in WATER_RECOVERY_SEARCH_EXPANSIONS:
var maximum_distance := nearest_surface_distance + expansion
var result: Variant = _nearest_safe_recovery_surface_position(
entry_position,
maximum_distance,
)
if result is Vector3:
return result as Vector3
var map_wide_result: Variant = _nearest_safe_recovery_surface_position(
entry_position,
get_playable_half_extents().length() * 2.0,
)
if map_wide_result is Vector3:
return map_wide_result as Vector3
return super(entry_position, fallback_position)
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
_clear_children(_decorations)
_clear_children(_tree_anchors)
_placed_prop_positions.clear()
_placed_prop_clearance_radii.clear()
_placed_prop_groups.clear()
_placed_group_coordinates.clear()
var random := RandomNumberGenerator.new()
random.seed = _current_seed ^ 0x5EED71
var grass_surface_triangles_by_coordinate := (
_terrain_surface_triangles_by_coordinate(
GRASS_PROP_SURFACE_MATERIALS,
)
)
var sand_surface_triangles_by_coordinate := (
_terrain_surface_triangles_by_coordinate(
SAND_PROP_SURFACE_MATERIALS,
)
)
var biomes: Array[TerrainBiomeDefinition] = []
if biome_catalog != null:
biomes.assign(biome_catalog.definitions)
var eligible_records: Dictionary[StringName, Array] = {}
var group_counts: Dictionary[StringName, int] = {}
for group: StringName in PROCEDURAL_PROP_GROUPS:
for biome: TerrainBiomeDefinition in biomes:
var rule := biome.prop_rule_for_group(group)
if rule == null:
continue
var group_key := _biome_group_key(biome.stable_id, group)
eligible_records[group_key] = []
group_counts[group_key] = 0
_placed_group_coordinates[group_key] = []
for record: Dictionary in records:
var position: Vector3 = record.get("position", Vector3.ZERO)
var tags: PackedStringArray = record.get(
"tags", PackedStringArray()
)
if "spawn" in tags:
spawn_position = position
continue
var coordinate: Vector2i = record.get(
"coordinate",
Vector2i.ZERO,
)
var biome_id: StringName = record.get("biome_id", &"")
if biome_catalog == null:
continue
var biome := biome_catalog.definition_for_id(biome_id)
if biome == null:
continue
for group: StringName in PROCEDURAL_PROP_GROUPS:
var rule := biome.prop_rule_for_group(group)
if rule == null:
continue
var definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
)
if definitions.is_empty():
continue
var group_key := _biome_group_key(biome_id, group)
var group_records: Array = eligible_records[group_key]
group_records.append(record)
for group: StringName in PROCEDURAL_PROP_GROUPS:
for biome: TerrainBiomeDefinition in biomes:
var rule := biome.prop_rule_for_group(group)
if rule == null:
continue
var group_key := _biome_group_key(biome.stable_id, group)
var group_records: Array = eligible_records[group_key]
var maximum := _prop_group_maximum(rule, group_records.size())
for record: Dictionary in group_records:
if group_counts.get(group_key, 0) >= maximum:
break
var coordinate: Vector2i = record.get(
"coordinate",
Vector2i.ZERO,
)
var tags: PackedStringArray = record.get(
"tags",
PackedStringArray(),
)
var terrain_triangles := _surface_triangles_for_coordinate(
(
sand_surface_triangles_by_coordinate
if group == &"sand_tree"
else grass_surface_triangles_by_coordinate
),
coordinate,
)
var definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
)
for _placement_attempt: int in (
rule.placement_attempts_per_chunk
):
if group_counts.get(group_key, 0) >= maximum:
break
if (
definitions.is_empty()
or not _prop_group_roll_succeeds(
rule,
group_key,
coordinate,
random,
)
):
continue
if _maybe_add_prop(
definitions,
record.get("position", Vector3.ZERO),
coordinate,
biome.stable_id,
int(record.get("ocean_facing_edges", 0)),
random,
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
_ensure_minimum_props(
eligible_records,
group_counts,
random,
grass_surface_triangles_by_coordinate,
sand_surface_triangles_by_coordinate,
)
_cache_water_recovery_surfaces(
grass_surface_triangles_by_coordinate,
sand_surface_triangles_by_coordinate,
)
_place_spawn_amenities(spawn_position)
_configure_fresh_water(records)
_configure_diggable_area()
# Imported prop instances may finish applying their scene state while the
# generated world is assembled. Mark the final decoration tree once every
# prop and material variant is in place so rain canopy occlusion persists.
_mark_precipitation_occluders()
world_generated.emit(_current_seed, summary)
func _mark_precipitation_occluders() -> void:
for prop: Node in _decorations.get_children():
var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName
if prop_group not in [&"grass_tree", &"sand_tree"]:
continue
PrecipitationOcclusionType.mark_tree_meshes(prop)
func _assign_biomes(
records: Array[Dictionary],
summary: Dictionary,
) -> void:
_biome_assignments = TerrainBiomeAssigner.assign(
biome_catalog,
records,
_current_seed,
)
for index: int in records.size():
var record := records[index]
var coordinate: Vector2i = record.get(
"coordinate",
Vector2i.ZERO,
)
record["biome_id"] = _biome_assignments.get(coordinate, &"")
records[index] = record
var generated_chunks := _generator.get_generated_chunks_root()
if generated_chunks != null:
for child: Node in generated_chunks.get_children():
var coordinate: Vector2i = child.get_meta(
&"terrain_chunk_coordinate",
Vector2i.ZERO,
)
child.set_meta(
&"terrain_biome_id",
_biome_assignments.get(coordinate, &""),
)
summary["biome_counts"] = TerrainBiomeAssigner.counts(
_biome_assignments
)
summary["biome_fingerprint"] = TerrainBiomeAssigner.fingerprint(
_biome_assignments
)
func _place_spawn_amenities(center: Vector3) -> void:
_player_spawn.position = center + Vector3(0.0, 0.18, 2.2)
_safe_spawn.position = _player_spawn.position
_shop_root.position = center + Vector3(2.35, 0.0, -1.35)
_shop_root.rotation.y = PI
_storage_root.position = center + Vector3(-2.4, 0.0, 1.2)
_storage_root.rotation.y = PI * 0.5
func _configure_static_water() -> void:
_ocean.water_type = WaterType.Type.SALT_WATER
_ocean.fish_pool = OCEAN_POOL
_ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"]
_ocean.surface_size = Vector2(10000.0, 10000.0)
_ocean.position.y = WATER_HEIGHT
_apply_water_materials()
func _configure_fresh_water(records: Array[Dictionary]) -> void:
_clear_children(_fresh_water_root)
for record: Dictionary in records:
var tags: PackedStringArray = record.get(
"tags", PackedStringArray()
)
if "fresh_water" not in tags:
continue
var surface_size: Vector2 = record.get(
"water_surface_size", Vector2.ZERO
)
var surface_polygon: PackedVector2Array = record.get(
"water_surface_polygon",
PackedVector2Array(),
)
if (
(surface_size.x <= 0.0 or surface_size.y <= 0.0)
and surface_polygon.size() < 3
):
continue
var body := WATER_BODY_SCENE.instantiate() as WaterBodyAuthoring
if body == null:
continue
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
body.name = "FreshWater_%d_%d" % [coordinate.x, coordinate.y]
_fresh_water_root.add_child(body)
var turns := int(record.get("rotation_quarters", 0))
var angle := float(posmod(turns, 4)) * PI * 0.5
var offset: Vector2 = record.get(
"water_surface_offset", Vector2.ZERO
)
var rotated_offset := Vector3(offset.x, 0.0, offset.y).rotated(
Vector3.UP,
angle,
)
body.position = (
record.get("position", Vector3.ZERO)
+ rotated_offset
+ Vector3.UP * WATER_HEIGHT
)
body.rotation.y = angle
if surface_polygon.size() >= 3:
body.surface_polygon = surface_polygon
else:
body.surface_size = surface_size
_add_generated_fresh_water_bed(
body,
surface_polygon,
surface_size,
)
body.visual_surface_enabled = false
body.water_material = _fresh_water_material()
body.water_type = WaterType.Type.FRESH_WATER
body.recovery_entry_height_reference = (
PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN
)
body.recovery_entry_depth_threshold = 0.1
body.recovery_entry_confirmation_seconds = 0.2
body.fish_pool = _fresh_water_pool(tags)
body.location_tags = _fresh_water_location_tags(tags)
body.selection_priority = 10
func _add_generated_fresh_water_bed(
body: WaterBodyAuthoring,
surface_polygon: PackedVector2Array,
surface_size: Vector2,
) -> void:
var polygon := surface_polygon
if polygon.size() < 3:
var half_size := surface_size * 0.5
polygon = PackedVector2Array([
Vector2(-half_size.x, -half_size.y),
Vector2(half_size.x, -half_size.y),
Vector2(half_size.x, half_size.y),
Vector2(-half_size.x, half_size.y),
])
var triangle_indices := Geometry2D.triangulate_polygon(polygon)
if triangle_indices.size() < 3:
return
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
for offset: int in range(0, triangle_indices.size(), 3):
var triangle := PackedVector3Array()
for point_offset: int in 3:
var point := polygon[triangle_indices[offset + point_offset]]
triangle.append(Vector3(point.x, 0.0, point.y))
if (
(triangle[1] - triangle[0]).cross(
triangle[2] - triangle[0]
).dot(Vector3.UP) < 0.0
):
var swap := triangle[1]
triangle[1] = triangle[2]
triangle[2] = swap
vertices.append_array(triangle)
normals.append_array(PackedVector3Array([
Vector3.UP,
Vector3.UP,
Vector3.UP,
]))
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
var bed := MeshInstance3D.new()
bed.name = "GeneratedBed"
bed.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
bed.position.y = GENERATED_FRESH_WATER_BED_HEIGHT - WATER_HEIGHT
bed.material_override = GENERATED_TERRAIN_DIRT_MATERIAL
bed.mesh = mesh
body.add_child(bed)
func _fresh_water_location_tags(tags: PackedStringArray) -> Array[StringName]:
var result: Array[StringName] = [&"generated_fresh_water"]
if "pond" in tags:
result.append(&"pond")
if "river" in tags:
result.append(&"river")
if "lake" in tags:
result.append(&"lake")
return result
func _fresh_water_pool(tags: PackedStringArray) -> FishPool:
if "river" in tags:
return GENERATED_RIVER_POOL
if "lake" in tags:
return GENERATED_LAKE_POOL
return GENERATED_POND_POOL
func _configure_diggable_area() -> void:
var half_extents := get_playable_half_extents()
_diggable_beach.generation_bounds = Rect2(
-half_extents,
half_extents * 2.0,
)
func _apply_water_materials() -> void:
if not is_node_ready():
return
if _light_performance_profile:
_ocean.water_material = _light_water_material(
Color(0.11, 0.345, 0.435),
)
else:
_ocean.water_material = SALT_WATER_MATERIAL
for child: Node in _fresh_water_root.get_children():
var body := child as WaterBodyAuthoring
if body != null:
body.water_material = _fresh_water_material()
func _fresh_water_material() -> Material:
if _light_performance_profile:
return _light_water_material(Color(0.18, 0.46, 0.50))
return FRESH_WATER_MATERIAL
func _light_water_material(color: Color) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.albedo_color = color
material.roughness = 1.0
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
return material
func _maybe_add_prop(
definitions: Array[TerrainPropDefinition],
chunk_center: Vector3,
chunk_coordinate: Vector2i,
biome_id: StringName,
ocean_facing_edges: int,
random: RandomNumberGenerator,
terrain_triangles: Array[PackedVector3Array],
) -> bool:
var definition := _pick_prop_definition(
definitions,
chunk_center,
random,
)
if definition == null:
return false
return _add_prop(
definition,
chunk_center,
chunk_coordinate,
biome_id,
ocean_facing_edges,
random,
terrain_triangles,
)
func _add_prop(
definition: TerrainPropDefinition,
chunk_center: Vector3,
chunk_coordinate: Vector2i,
biome_id: StringName,
ocean_facing_edges: int,
random: RandomNumberGenerator,
terrain_triangles: Array[PackedVector3Array],
) -> bool:
if definition == null or definition.packed_scene == null:
return false
var cluster_size := random.randi_range(
definition.minimum_cluster_size,
definition.maximum_cluster_size,
)
var visual_scales := _prop_visual_scales(
definition,
cluster_size,
random,
)
var primary_scale := visual_scales[0]
var scaled_clearance := definition.clearance_radius * primary_scale
var support_radius := clampf(scaled_clearance * 0.35, 0.25, 0.65)
var placement := Vector3.ZERO
var found_surface := false
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
var offset := Vector3(
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
0.0,
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
)
placement = chunk_center + offset
var surface_height := _surface_height_at(
placement,
terrain_triangles,
)
if (
surface_height > -INF
and surface_height
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
and _surface_supports_prop_footprint(
placement,
surface_height,
support_radius,
terrain_triangles,
)
and _has_prop_clearance(
placement,
scaled_clearance,
)
):
placement.y = surface_height
found_surface = true
break
if not found_surface:
return false
var cluster_id := _decorations.get_child_count()
if not _instantiate_prop(
definition,
placement,
primary_scale,
_prop_yaw(definition, ocean_facing_edges, placement, random),
chunk_coordinate,
biome_id,
cluster_id,
0,
random,
):
return false
if cluster_size <= 1:
return true
var cluster_angle := random.randf_range(-PI, PI)
var companion_count := cluster_size - 1
var chunk_half_extent := _generator.catalog.chunk_size * 0.5 - 0.25
for member_index: int in range(1, cluster_size):
var member_scale := visual_scales[member_index]
var member_clearance := definition.clearance_radius * member_scale
var member_support_radius := clampf(
member_clearance * 0.35,
0.25,
0.65,
)
for _attempt: int in PROP_CLUSTER_PLACEMENT_ATTEMPTS:
var sector_angle := (
cluster_angle
+ TAU * float(member_index - 1) / float(companion_count)
+ random.randf_range(-0.45, 0.45)
)
var radius := random.randf_range(
definition.minimum_cluster_radius,
definition.maximum_cluster_radius,
)
var candidate := placement + Vector3(
cos(sector_angle) * radius,
0.0,
sin(sector_angle) * radius,
)
if (
absf(candidate.x - chunk_center.x) > chunk_half_extent
or absf(candidate.z - chunk_center.z) > chunk_half_extent
):
continue
var surface_height := _surface_height_at(
candidate,
terrain_triangles,
)
if (
surface_height <= WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
or not _surface_supports_prop_footprint(
candidate,
surface_height,
member_support_radius,
terrain_triangles,
)
or not _has_prop_clearance(candidate, member_clearance)
):
continue
candidate.y = surface_height
if _instantiate_prop(
definition,
candidate,
member_scale,
_prop_yaw(
definition,
ocean_facing_edges,
candidate,
random,
),
chunk_coordinate,
biome_id,
cluster_id,
member_index,
random,
):
break
return true
func _prop_visual_scales(
definition: TerrainPropDefinition,
count: int,
random: RandomNumberGenerator,
) -> Array[float]:
var result: Array[float] = []
if count <= 1:
result.append(
random.randf_range(
definition.minimum_visual_scale,
definition.maximum_visual_scale,
)
)
return result
for index: int in count:
var descending_band := count - index - 1
var band_minimum := lerpf(
definition.minimum_visual_scale,
definition.maximum_visual_scale,
float(descending_band) / float(count),
)
var band_maximum := lerpf(
definition.minimum_visual_scale,
definition.maximum_visual_scale,
float(descending_band + 1) / float(count),
)
result.append(random.randf_range(band_minimum, band_maximum))
return result
func _instantiate_prop(
definition: TerrainPropDefinition,
placement: Vector3,
visual_scale: float,
yaw: float,
chunk_coordinate: Vector2i,
biome_id: StringName,
cluster_id: int,
cluster_member_index: int,
random: RandomNumberGenerator,
) -> bool:
var packed_instance := definition.packed_scene.instantiate()
var visual_root := packed_instance as Node3D
if visual_root == null:
packed_instance.free()
return false
var prop := Node3D.new()
prop.name = "%s_%d" % [
String(definition.stable_id).trim_prefix("prop_"),
_decorations.get_child_count(),
]
prop.set_meta(&"terrain_prop_id", definition.stable_id)
prop.set_meta(&"terrain_prop_group", definition.procedural_group)
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
prop.set_meta(&"terrain_biome_id", biome_id)
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
prop.set_meta(&"terrain_prop_cluster_id", cluster_id)
prop.set_meta(&"terrain_prop_cluster_member_index", cluster_member_index)
prop.position = placement
prop.rotation.y = yaw
_decorations.add_child(prop)
visual_root.name = "Visual"
prop.add_child(visual_root)
visual_root.position = definition.visual_offset
visual_root.scale = Vector3.ONE * visual_scale
_configure_prop_visuals(visual_root)
_apply_prop_material_variant(visual_root, definition, random)
_add_prop_collision(prop, definition, visual_scale)
_placed_prop_positions.append(placement)
_placed_prop_clearance_radii.append(
definition.clearance_radius * visual_scale
)
_placed_prop_groups.append(definition.procedural_group)
if definition.has_gatherable_surface():
var authored_sockets := _authored_beetle_sockets(visual_root)
for socket: Node3D in authored_sockets:
_add_tree_gatherable_anchor(
prop,
socket.global_position,
definition,
)
return true
func _authored_beetle_sockets(root: Node) -> Array[Node3D]:
var sockets: Array[Node3D] = []
var node_3d := root as Node3D
if (
node_3d != null
and String(node_3d.name).to_lower().contains("beetle_socket")
):
sockets.append(node_3d)
for child: Node in root.get_children():
sockets.append_array(_authored_beetle_sockets(child))
return sockets
func _add_tree_gatherable_anchor(
prop: Node3D,
global_anchor_position: Vector3,
definition: TerrainPropDefinition,
) -> void:
var anchor := Marker3D.new()
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
anchor.position = _tree_anchors.to_local(global_anchor_position)
anchor.set_meta(&"terrain_prop_id", definition.stable_id)
anchor.set_meta(&"terrain_prop_name", prop.name)
anchor.set_meta(&"authored_beetle_socket", true)
_tree_anchors.add_child(anchor)
func _prop_yaw(
definition: TerrainPropDefinition,
ocean_facing_edges: int,
placement: Vector3,
random: RandomNumberGenerator,
) -> float:
if not definition.prefer_ocean_facing:
return random.randf_range(-PI, PI)
var ocean_direction := Vector2.ZERO
for edge_value: int in TerrainChunkTopology.Edge.values():
if (ocean_facing_edges & (1 << edge_value)) == 0:
continue
var edge_normal := TerrainChunkTopology.edge_normal(
edge_value as TerrainChunkTopology.Edge
)
ocean_direction += Vector2(edge_normal.x, edge_normal.z)
if ocean_direction.is_zero_approx():
ocean_direction = _nearest_ocean_direction(placement)
var local_direction := definition.local_overhang_direction.normalized()
var target_angle := atan2(ocean_direction.x, ocean_direction.y)
var local_angle := atan2(local_direction.x, local_direction.y)
var spread := deg_to_rad(definition.ocean_facing_spread_degrees)
return (
target_angle
- local_angle
+ random.randf_range(-spread, spread)
)
func _nearest_ocean_direction(position: Vector3) -> Vector2:
var half_extents := get_playable_half_extents()
var distance_x := half_extents.x - absf(position.x)
var distance_z := half_extents.y - absf(position.z)
var direction_x := Vector2.RIGHT if position.x >= 0.0 else Vector2.LEFT
var direction_z := Vector2.DOWN if position.z >= 0.0 else Vector2.UP
if absf(distance_x - distance_z) <= _generator.catalog.chunk_size * 0.35:
return (direction_x + direction_z).normalized()
return direction_x if distance_x < distance_z else direction_z
func _validate_prop_catalog() -> void:
if prop_catalog == null:
push_error("Generated world terrain prop catalog is unavailable.")
return
for error: String in prop_catalog.validation_errors():
push_error("Generated world terrain prop catalog: %s" % error)
func _validate_biome_catalog() -> void:
if biome_catalog == null:
push_error("Generated world terrain biome catalog is unavailable.")
return
for error: String in biome_catalog.validation_errors():
push_error("Generated world terrain biome catalog: %s" % error)
if prop_catalog == null:
return
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
if biome == null:
continue
for rule: TerrainBiomePropRule in biome.prop_rules:
if rule == null:
continue
if rule.procedural_group not in PROCEDURAL_PROP_GROUPS:
push_error(
"Biome %s references unsupported prop group %s."
% [biome.stable_id, rule.procedural_group]
)
for prop_id_value: String in rule.allowed_prop_ids:
var prop_id := StringName(prop_id_value)
var definition := prop_catalog.definition_for_id(prop_id)
if definition == null:
push_error(
"Biome %s references missing prop %s."
% [biome.stable_id, prop_id]
)
elif definition.procedural_group != rule.procedural_group:
push_error(
"Biome %s assigns prop %s to the wrong group."
% [biome.stable_id, prop_id]
)
func _prop_definitions_for(
rule: TerrainBiomePropRule,
tags: PackedStringArray,
) -> Array[TerrainPropDefinition]:
if prop_catalog == null or rule == null:
return []
var result: Array[TerrainPropDefinition] = []
for definition: TerrainPropDefinition in (
prop_catalog.procedural_definitions(rule.procedural_group, tags)
):
if rule.allows_prop(definition):
result.append(definition)
return result
func _prop_group_roll_succeeds(
rule: TerrainBiomePropRule,
group_key: StringName,
coordinate: Vector2i,
random: RandomNumberGenerator,
) -> bool:
var threshold := rule.placement_chance
if _group_has_adjacent_placement(group_key, coordinate):
threshold += rule.adjacency_bonus
threshold = mini(threshold, PROP_CHANCE_SCALE)
return random.randi_range(0, PROP_CHANCE_SCALE - 1) < threshold
func _pick_prop_definition(
definitions: Array[TerrainPropDefinition],
chunk_center: Vector3,
random: RandomNumberGenerator,
) -> TerrainPropDefinition:
var total_weight := 0
var weights: Array[int] = []
for definition: TerrainPropDefinition in definitions:
var weight := maxi(
1,
roundi(
definition.selection_weight
* float(PROP_SELECTION_WEIGHT_SCALE)
),
)
if not definition.preferred_nearby_prop_groups.is_empty():
var multiplier := definition.nearby_preference_weight_multiplier
if _has_preferred_prop_near(definition, chunk_center):
weight = maxi(roundi(float(weight) * multiplier), 1)
else:
weight = maxi(roundi(float(weight) / multiplier), 1)
weights.append(weight)
total_weight += weight
if total_weight <= 0:
return null
var roll := random.randi_range(1, total_weight)
for index: int in definitions.size():
roll -= weights[index]
if roll <= 0:
return definitions[index]
return definitions.back() if not definitions.is_empty() else null
func _prop_group_maximum(
rule: TerrainBiomePropRule,
eligible_count: int,
) -> int:
if eligible_count <= 0:
return 0
var maximum := ceili(
float(
eligible_count
* rule.placement_attempts_per_chunk
* rule.maximum_density
)
/ float(PROP_CHANCE_SCALE)
)
return maxi(maximum, rule.minimum_placements)
func _spawn_distance_eligible_definitions(
definitions: Array[TerrainPropDefinition],
coordinate: Vector2i,
) -> Array[TerrainPropDefinition]:
var result: Array[TerrainPropDefinition] = []
var center := Vector2i(
_generator.grid_size.x / 2,
_generator.grid_size.y / 2,
)
var distance := (
absi(coordinate.x - center.x)
+ absi(coordinate.y - center.y)
)
for definition: TerrainPropDefinition in definitions:
if distance >= definition.minimum_spawn_chunk_distance:
result.append(definition)
return result
func _group_has_adjacent_placement(
group: StringName,
coordinate: Vector2i,
) -> bool:
var coordinates: Array = _placed_group_coordinates.get(group, [])
for coordinate_value: Variant in coordinates:
var other: Vector2i = coordinate_value
if (
absi(coordinate.x - other.x)
+ absi(coordinate.y - other.y)
== 1
):
return true
return false
func _record_prop_group_coordinate(
group: StringName,
coordinate: Vector2i,
) -> void:
var coordinates: Array = _placed_group_coordinates.get(group, [])
coordinates.append(coordinate)
_placed_group_coordinates[group] = coordinates
func _has_preferred_prop_near(
definition: TerrainPropDefinition,
position: Vector3,
) -> bool:
for index: int in _placed_prop_positions.size():
if (
_placed_prop_groups[index]
not in definition.preferred_nearby_prop_groups
):
continue
var other := _placed_prop_positions[index]
if Vector2(position.x - other.x, position.z - other.z).length() <= (
definition.preferred_nearby_radius
):
return true
return false
func _ensure_minimum_props(
eligible_records: Dictionary[StringName, Array],
group_counts: Dictionary[StringName, int],
random: RandomNumberGenerator,
grass_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
sand_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
) -> void:
if biome_catalog == null:
return
for group: StringName in PROCEDURAL_PROP_GROUPS:
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
var rule := biome.prop_rule_for_group(group)
if rule == null or rule.minimum_placements <= 0:
continue
var group_key := _biome_group_key(biome.stable_id, group)
var records: Array = eligible_records.get(group_key, [])
if records.is_empty():
continue
var maximum_attempts := maxi(
PROP_PLACEMENT_ATTEMPTS,
records.size() * rule.minimum_placements * 2,
)
for _attempt: int in maximum_attempts:
if (
group_counts.get(group_key, 0)
>= rule.minimum_placements
):
break
var record: Dictionary = records[
random.randi_range(0, records.size() - 1)
]
var tags: PackedStringArray = record.get(
"tags",
PackedStringArray(),
)
var coordinate: Vector2i = record.get(
"coordinate",
Vector2i.ZERO,
)
var terrain_triangles := _surface_triangles_for_coordinate(
(
sand_surface_triangles_by_coordinate
if group == &"sand_tree"
else grass_surface_triangles_by_coordinate
),
coordinate,
)
var definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
)
if _maybe_add_prop(
definitions,
record.get("position", Vector3.ZERO),
coordinate,
biome.stable_id,
int(record.get("ocean_facing_edges", 0)),
random,
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
func _biome_group_key(
biome_id: StringName,
group: StringName,
) -> StringName:
return StringName("%s:%s" % [biome_id, group])
func _has_prop_clearance(position: Vector3, radius: float) -> bool:
for index: int in _placed_prop_positions.size():
var other := _placed_prop_positions[index]
var distance := Vector2(
position.x - other.x,
position.z - other.z,
).length()
if distance < radius + _placed_prop_clearance_radii[index]:
return false
return true
func _configure_prop_visuals(root_node: Node) -> void:
if root_node is GeometryInstance3D:
(root_node as GeometryInstance3D).cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
for child: Node in root_node.get_children():
_configure_prop_visuals(child)
func _apply_prop_material_variant(
root_node: Node,
definition: TerrainPropDefinition,
random: RandomNumberGenerator,
) -> void:
_apply_random_material_variant(
root_node,
definition.variant_material_slot_names,
definition.material_variants,
random,
)
_apply_random_material_variant(
root_node,
definition.secondary_variant_material_slot_names,
definition.secondary_material_variants,
random,
)
func _apply_random_material_variant(
root_node: Node,
target_material_names: PackedStringArray,
variants: Array[Material],
random: RandomNumberGenerator,
) -> void:
if variants.is_empty() or target_material_names.is_empty():
return
var variant := variants[random.randi_range(0, variants.size() - 1)]
_apply_material_variant_to_meshes(
root_node,
target_material_names,
variant,
)
func _apply_material_variant_to_meshes(
root_node: Node,
target_material_names: PackedStringArray,
variant: Material,
) -> void:
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var active_material := mesh_instance.get_active_material(surface_index)
if (
active_material != null
and active_material.resource_name in target_material_names
):
mesh_instance.set_surface_override_material(
surface_index,
variant,
)
for child: Node in root_node.get_children():
_apply_material_variant_to_meshes(
child,
target_material_names,
variant,
)
func _terrain_surface_triangles_by_coordinate(
material_names: Array[StringName],
) -> Dictionary[Vector2i, Array]:
var triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
if material_names.is_empty():
return triangles_by_coordinate
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
if mesh_instance.mesh == null:
continue
var coordinate := _terrain_coordinate_for(mesh_instance)
if coordinate.x < 0 or coordinate.y < 0:
continue
var triangles: Array[PackedVector3Array] = []
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.get_active_material(surface_index)
if (
material == null
or StringName(material.resource_name) not in material_names
):
continue
_append_surface_triangles(
triangles,
mesh_instance,
mesh_instance.mesh.surface_get_arrays(surface_index),
-INF,
0.35,
)
if triangles.is_empty():
continue
var coordinate_triangles: Array = triangles_by_coordinate.get(
coordinate,
[],
)
coordinate_triangles.append_array(triangles)
triangles_by_coordinate[coordinate] = coordinate_triangles
return triangles_by_coordinate
func _cache_water_recovery_surfaces(
grass_triangles: Dictionary[Vector2i, Array],
sand_triangles: Dictionary[Vector2i, Array],
) -> void:
_water_recovery_triangles_by_coordinate.clear()
_append_water_recovery_surfaces(grass_triangles)
_append_water_recovery_surfaces(sand_triangles)
func _append_water_recovery_surfaces(
source: Dictionary[Vector2i, Array],
) -> void:
for coordinate: Vector2i in source:
var eligible: Array = _water_recovery_triangles_by_coordinate.get(
coordinate,
[],
)
for triangle: PackedVector3Array in source[coordinate]:
if triangle.size() != 3:
continue
if minf(
triangle[0].y,
minf(triangle[1].y, triangle[2].y),
) <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE:
continue
var cross := (triangle[1] - triangle[0]).cross(
triangle[2] - triangle[0]
)
if (
cross.length_squared() <= 0.0000001
or absf(cross.normalized().dot(Vector3.UP))
< WATER_RECOVERY_MINIMUM_UP_DOT
):
continue
eligible.append(triangle)
if not eligible.is_empty():
_water_recovery_triangles_by_coordinate[coordinate] = eligible
func _nearest_recovery_surface_distance(entry_position: Vector3) -> float:
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
var nearest_distance_squared := INF
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
for triangle: PackedVector3Array in (
_water_recovery_triangles_by_coordinate[coordinate]
):
var closest := _closest_horizontal_point_on_triangle(
entry_horizontal,
triangle,
)
nearest_distance_squared = minf(
nearest_distance_squared,
entry_horizontal.distance_squared_to(closest),
)
return sqrt(nearest_distance_squared)
func _nearest_safe_recovery_surface_position(
entry_position: Vector3,
maximum_distance: float,
) -> Variant:
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
var maximum_distance_squared := maximum_distance * maximum_distance
var best_distance_squared := INF
var best_position: Variant = null
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
for triangle: PackedVector3Array in (
_water_recovery_triangles_by_coordinate[coordinate]
):
var closest := _closest_horizontal_point_on_triangle(
entry_horizontal,
triangle,
)
if (
entry_horizontal.distance_squared_to(closest)
> maximum_distance_squared
):
continue
var centroid := Vector2(
(triangle[0].x + triangle[1].x + triangle[2].x) / 3.0,
(triangle[0].z + triangle[1].z + triangle[2].z) / 3.0,
)
var inset_distance := minf(
closest.distance_to(centroid),
WATER_RECOVERY_MAXIMUM_INSET,
)
var inset_steps := ceili(
inset_distance / WATER_RECOVERY_INSET_STEP
)
for inset_index: int in inset_steps + 1:
var candidate_horizontal := closest.move_toward(
centroid,
minf(
float(inset_index) * WATER_RECOVERY_INSET_STEP,
inset_distance,
),
)
var distance_squared := entry_horizontal.distance_squared_to(
candidate_horizontal
)
if (
distance_squared > maximum_distance_squared
or distance_squared >= best_distance_squared
):
continue
var candidate := Vector3(
candidate_horizontal.x,
entry_position.y,
candidate_horizontal.y,
)
var safe_position: Variant = _safe_recovery_surface_position(
candidate
)
if safe_position is Vector3:
best_distance_squared = distance_squared
best_position = safe_position
return best_position
func _safe_recovery_surface_position(candidate: Vector3) -> Variant:
var nearby_triangles := _water_recovery_surface_triangles_near(candidate)
var surface_height := _surface_height_at(candidate, nearby_triangles)
if (
surface_height <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
or not _surface_supports_prop_footprint(
candidate,
surface_height,
WATER_RECOVERY_FOOTPRINT_RADIUS,
nearby_triangles,
)
or not _has_prop_clearance(
candidate,
WATER_RECOVERY_FOOTPRINT_RADIUS,
)
):
return null
candidate.y = surface_height
return candidate
func _water_recovery_surface_triangles_near(
position: Vector3,
) -> Array[PackedVector3Array]:
var result: Array[PackedVector3Array] = []
if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0:
return result
var local_position := _generator.to_local(position)
var half_grid := Vector2(
float(_generator.grid_size.x - 1) * 0.5,
float(_generator.grid_size.y - 1) * 0.5,
)
var coordinate := Vector2i(
roundi(local_position.x / _generator.catalog.chunk_size + half_grid.x),
roundi(local_position.z / _generator.catalog.chunk_size + half_grid.y),
)
for offset_x: int in range(-1, 2):
for offset_y: int in range(-1, 2):
var nearby_coordinate := coordinate + Vector2i(offset_x, offset_y)
result.append_array(
_water_recovery_triangles_by_coordinate.get(
nearby_coordinate,
[],
)
)
return result
func _closest_horizontal_point_on_triangle(
point: Vector2,
triangle: PackedVector3Array,
) -> Vector2:
var polygon := PackedVector2Array([
Vector2(triangle[0].x, triangle[0].z),
Vector2(triangle[1].x, triangle[1].z),
Vector2(triangle[2].x, triangle[2].z),
])
if Geometry2D.is_point_in_polygon(point, polygon):
return point
var closest := Geometry2D.get_closest_point_to_segment(
point,
polygon[0],
polygon[1],
)
var nearest_distance_squared := point.distance_squared_to(closest)
for edge_index: int in range(1, 3):
var edge_closest := Geometry2D.get_closest_point_to_segment(
point,
polygon[edge_index],
polygon[(edge_index + 1) % 3],
)
var distance_squared := point.distance_squared_to(edge_closest)
if distance_squared < nearest_distance_squared:
nearest_distance_squared = distance_squared
closest = edge_closest
return closest
func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i:
var current: Node = mesh_instance
while current != null and current != _generator:
if current.has_meta(&"terrain_chunk_coordinate"):
return current.get_meta(
&"terrain_chunk_coordinate",
Vector2i(-1, -1),
) as Vector2i
current = current.get_parent()
return Vector2i(-1, -1)
func _surface_triangles_for_coordinate(
triangles_by_coordinate: Dictionary[Vector2i, Array],
coordinate: Vector2i,
) -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
triangles.assign(triangles_by_coordinate.get(coordinate, []))
return triangles
func _surface_height_at(
position: Vector3,
triangles: Array[PackedVector3Array],
) -> float:
var highest := -INF
var segment_start := position + Vector3.UP * 100.0
var segment_end := position + Vector3.DOWN * 100.0
for triangle: PackedVector3Array in triangles:
if triangle.size() != 3:
continue
var hit: Variant = Geometry3D.segment_intersects_triangle(
segment_start,
segment_end,
triangle[0],
triangle[1],
triangle[2],
)
if hit is Vector3:
highest = maxf(highest, (hit as Vector3).y)
return highest
func _surface_supports_prop_footprint(
position: Vector3,
surface_height: float,
radius: float,
triangles: Array[PackedVector3Array],
) -> bool:
for direction: Vector2 in PROP_SURFACE_SAMPLE_DIRECTIONS:
var sample_position := position + Vector3(
direction.x * radius,
0.0,
direction.y * radius,
)
var sample_height := _surface_height_at(sample_position, triangles)
if (
sample_height <= -INF
or absf(sample_height - surface_height)
> PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
):
return false
return true
func _add_prop_collision(
prop: Node3D,
definition: TerrainPropDefinition,
visual_scale: float,
) -> void:
if not definition.has_collision():
return
var body := StaticBody3D.new()
body.name = "TrunkCollision"
body.collision_layer = 1
body.collision_mask = 0
prop.add_child(body)
var collision := CollisionShape3D.new()
collision.name = "CollisionShape"
if definition.has_box_collision():
var box := BoxShape3D.new()
box.size = definition.collision_box_size * visual_scale
collision.shape = box
collision.position = definition.collision_offset * visual_scale
else:
var cylinder := CylinderShape3D.new()
cylinder.radius = definition.collision_radius * visual_scale
cylinder.height = definition.collision_height * visual_scale
collision.shape = cylinder
collision.position = (
definition.collision_offset * visual_scale
+ Vector3.UP * cylinder.height * 0.5
)
body.add_child(collision)
func _build_shoreline_reference() -> void:
var half := get_playable_half_extents()
var width := 0.05
var vertices := PackedVector3Array()
var indices := PackedInt32Array()
var corners := [
Vector3(-half.x, WATER_HEIGHT, -half.y),
Vector3(half.x, WATER_HEIGHT, -half.y),
Vector3(half.x, WATER_HEIGHT, half.y),
Vector3(-half.x, WATER_HEIGHT, half.y),
]
for index: int in 4:
var start: Vector3 = corners[index]
var finish: Vector3 = corners[(index + 1) % 4]
var direction := (finish - start).normalized()
var perpendicular := Vector3(-direction.z, 0.0, direction.x) * width
var base := vertices.size()
vertices.append_array(PackedVector3Array([
start - perpendicular,
start + perpendicular,
finish + perpendicular,
finish - perpendicular,
]))
indices.append_array(PackedInt32Array([
base, base + 1, base + 2,
base, base + 2, base + 3,
]))
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
_shoreline_reference.mesh = mesh
func _append_surface_triangles(
result: Array[PackedVector3Array],
mesh_instance: MeshInstance3D,
arrays: Array,
minimum_global_y: float,
minimum_up_dot: float,
) -> void:
if arrays.size() <= Mesh.ARRAY_INDEX:
return
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
if vertices.is_empty():
return
if indices.is_empty():
for index: int in range(0, vertices.size() - 2, 3):
_append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot)
return
for index: int in range(0, indices.size() - 2, 3):
_append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot)
func _append_triangle(
result: Array[PackedVector3Array],
mesh_instance: MeshInstance3D,
local_a: Vector3,
local_b: Vector3,
local_c: Vector3,
minimum_global_y: float,
minimum_up_dot: float,
) -> void:
var a := mesh_instance.to_global(local_a)
var b := mesh_instance.to_global(local_b)
var c := mesh_instance.to_global(local_c)
if minf(a.y, minf(b.y, c.y)) <= minimum_global_y:
return
var cross := (b - a).cross(c - a)
if cross.length_squared() <= 0.0000001:
return
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
return
result.append(PackedVector3Array([a, b, c]))
func _clear_children(root: Node) -> void:
for child: Node in root.get_children():
root.remove_child(child)
child.free()