forked from woofmeow/straywild
Expand generated terrain with biome-driven props
This commit is contained in:
parent
e8888ed05a
commit
1e24fb0bfc
103 changed files with 5427 additions and 351 deletions
|
|
@ -9,9 +9,6 @@ const FishingShopInteractionType = preload(
|
|||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
const PROP_SOURCE: PackedScene = preload(
|
||||
"res://art/exported/environment/terrain/starter_island.glb"
|
||||
)
|
||||
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
||||
const SALT_WATER_MATERIAL: Material = preload(
|
||||
"res://world/materials/stylized_water.tres"
|
||||
|
|
@ -25,16 +22,21 @@ const POND_POOL: FishPool = preload(
|
|||
const OCEAN_POOL: FishPool = preload(
|
||||
"res://fish/pools/starter_ocean_pool.tres"
|
||||
)
|
||||
const TREE_SOURCE_NAME := "tree_2_001"
|
||||
const PALM_SOURCE_NAME := "plam_tree"
|
||||
const TREE_RUNTIME_NAME := "regular_tree"
|
||||
const PALM_RUNTIME_NAME := "palm_tree"
|
||||
const WATER_HEIGHT := -0.25
|
||||
const PROP_EDGE_MARGIN := 2.2
|
||||
const TREE_CHANCE := 0.58
|
||||
const PALM_CHANCE := 0.42
|
||||
const PROP_PLACEMENT_ATTEMPTS := 12
|
||||
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||
const PROP_CHANCE_SCALE := 10000
|
||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||
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
|
||||
|
|
@ -56,15 +58,17 @@ const PALM_CHANCE := 0.42
|
|||
|
||||
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
var _light_performance_profile := false
|
||||
var _source_meshes: Dictionary[String, Mesh] = {}
|
||||
var _source_bases: Dictionary[String, Basis] = {}
|
||||
var _source_minimum_y: Dictionary[String, float] = {}
|
||||
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] = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_generator.generation_completed.connect(_on_generation_completed)
|
||||
_cache_prop_source(TREE_SOURCE_NAME)
|
||||
_cache_prop_source(PALM_SOURCE_NAME)
|
||||
_validate_prop_catalog()
|
||||
_validate_biome_catalog()
|
||||
_configure_static_water()
|
||||
_build_shoreline_reference()
|
||||
if not generate_world(initial_seed):
|
||||
|
|
@ -107,6 +111,18 @@ 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()
|
||||
|
|
@ -118,10 +134,9 @@ func get_spawn_surface_triangles(
|
|||
minimum_up_dot: float = 0.6,
|
||||
) -> Array[PackedVector3Array]:
|
||||
var triangles: Array[PackedVector3Array] = []
|
||||
var root: Node3D = _generator.get_generated_chunks_root()
|
||||
if root == null or material_names.is_empty():
|
||||
if material_names.is_empty():
|
||||
return triangles
|
||||
for mesh_instance: MeshInstance3D in _collect_mesh_instances(root):
|
||||
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
||||
var mesh: Mesh = mesh_instance.mesh
|
||||
if mesh == null:
|
||||
continue
|
||||
|
|
@ -143,15 +158,31 @@ func get_spawn_surface_triangles(
|
|||
|
||||
func _on_generation_completed(summary: Dictionary) -> void:
|
||||
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_centers: Array[Vector3] = []
|
||||
var sand_centers: Array[Vector3] = []
|
||||
var tree_count := 0
|
||||
var palm_count := 0
|
||||
var terrain_triangles := _terrain_surface_triangles()
|
||||
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(
|
||||
|
|
@ -159,50 +190,124 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
)
|
||||
if "spawn" in tags:
|
||||
spawn_position = position
|
||||
if "spawn" not in tags and "grass" in tags:
|
||||
grass_centers.append(position)
|
||||
if _maybe_add_prop(
|
||||
TREE_SOURCE_NAME,
|
||||
TREE_RUNTIME_NAME,
|
||||
position,
|
||||
TREE_CHANCE,
|
||||
random,
|
||||
true,
|
||||
):
|
||||
tree_count += 1
|
||||
elif "sand" in tags:
|
||||
sand_centers.append(position)
|
||||
if _maybe_add_prop(
|
||||
PALM_SOURCE_NAME,
|
||||
PALM_RUNTIME_NAME,
|
||||
position,
|
||||
PALM_CHANCE,
|
||||
random,
|
||||
false,
|
||||
):
|
||||
palm_count += 1
|
||||
if tree_count == 0 and not grass_centers.is_empty():
|
||||
_add_prop(
|
||||
TREE_SOURCE_NAME,
|
||||
TREE_RUNTIME_NAME,
|
||||
grass_centers[random.randi_range(0, grass_centers.size() - 1)],
|
||||
random,
|
||||
true,
|
||||
)
|
||||
if palm_count == 0 and not sand_centers.is_empty():
|
||||
_add_prop(
|
||||
PALM_SOURCE_NAME,
|
||||
PALM_RUNTIME_NAME,
|
||||
sand_centers[random.randi_range(0, sand_centers.size() - 1)],
|
||||
random,
|
||||
false,
|
||||
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 definitions := _spawn_distance_eligible_definitions(
|
||||
_prop_definitions_for(rule, tags),
|
||||
coordinate,
|
||||
)
|
||||
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,
|
||||
random,
|
||||
terrain_triangles,
|
||||
):
|
||||
group_counts[group_key] += 1
|
||||
_record_prop_group_coordinate(
|
||||
group_key,
|
||||
coordinate,
|
||||
)
|
||||
_ensure_minimum_props(
|
||||
eligible_records,
|
||||
group_counts,
|
||||
random,
|
||||
terrain_triangles,
|
||||
)
|
||||
_place_spawn_amenities(spawn_position)
|
||||
_configure_fresh_water(records)
|
||||
_configure_diggable_area()
|
||||
world_generated.emit(_current_seed, summary)
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -312,60 +417,410 @@ func _light_water_material(color: Color) -> StandardMaterial3D:
|
|||
|
||||
|
||||
func _maybe_add_prop(
|
||||
source_name: String,
|
||||
runtime_name: String,
|
||||
definitions: Array[TerrainPropDefinition],
|
||||
chunk_center: Vector3,
|
||||
chance: float,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
random: RandomNumberGenerator,
|
||||
add_anchor: bool,
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
if random.randf() > chance or not _source_meshes.has(source_name):
|
||||
return false
|
||||
_add_prop(
|
||||
source_name,
|
||||
runtime_name,
|
||||
var definition := _pick_prop_definition(
|
||||
definitions,
|
||||
chunk_center,
|
||||
random,
|
||||
add_anchor,
|
||||
)
|
||||
return true
|
||||
if definition == null:
|
||||
return false
|
||||
return _add_prop(
|
||||
definition,
|
||||
chunk_center,
|
||||
chunk_coordinate,
|
||||
biome_id,
|
||||
random,
|
||||
terrain_triangles,
|
||||
)
|
||||
|
||||
|
||||
func _add_prop(
|
||||
source_name: String,
|
||||
runtime_name: String,
|
||||
definition: TerrainPropDefinition,
|
||||
chunk_center: Vector3,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
random: RandomNumberGenerator,
|
||||
add_anchor: bool,
|
||||
) -> void:
|
||||
if not _source_meshes.has(source_name):
|
||||
return
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
if definition == null or definition.packed_scene == null:
|
||||
return false
|
||||
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 _has_prop_clearance(
|
||||
placement,
|
||||
definition.clearance_radius,
|
||||
)
|
||||
):
|
||||
placement.y = surface_height
|
||||
found_surface = true
|
||||
break
|
||||
if not found_surface:
|
||||
return false
|
||||
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" % [runtime_name, _decorations.get_child_count()]
|
||||
var offset := Vector3(
|
||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||
0.0,
|
||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||
)
|
||||
prop.position = chunk_center + offset
|
||||
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.position = placement
|
||||
prop.rotation.y = random.randf_range(-PI, PI)
|
||||
_decorations.add_child(prop)
|
||||
var visual := MeshInstance3D.new()
|
||||
visual.name = "Visual"
|
||||
visual.mesh = _source_meshes[source_name]
|
||||
visual.basis = _source_bases[source_name]
|
||||
visual.position.y = -_source_minimum_y.get(source_name, 0.0)
|
||||
visual.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
prop.add_child(visual)
|
||||
_add_prop_collision(prop, source_name == TREE_SOURCE_NAME)
|
||||
if add_anchor:
|
||||
visual_root.name = "Visual"
|
||||
prop.add_child(visual_root)
|
||||
_configure_prop_visuals(visual_root)
|
||||
_add_prop_collision(prop, definition)
|
||||
_placed_prop_positions.append(placement)
|
||||
_placed_prop_clearance_radii.append(definition.clearance_radius)
|
||||
_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()
|
||||
anchor.position = prop.position + Vector3(0.0, 2.15, 0.0)
|
||||
anchor.position = prop.position + Vector3(
|
||||
0.0,
|
||||
definition.gatherable_anchor_height,
|
||||
0.0,
|
||||
)
|
||||
_tree_anchors.add_child(anchor)
|
||||
return true
|
||||
|
||||
|
||||
func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
|
||||
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.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,
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> 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 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,
|
||||
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 _terrain_surface_triangles() -> Array[PackedVector3Array]:
|
||||
var triangles: Array[PackedVector3Array] = []
|
||||
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
||||
if mesh_instance.mesh == null:
|
||||
continue
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
_append_surface_triangles(
|
||||
triangles,
|
||||
mesh_instance,
|
||||
mesh_instance.mesh.surface_get_arrays(surface_index),
|
||||
-INF,
|
||||
0.35,
|
||||
)
|
||||
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 _add_prop_collision(
|
||||
prop: Node3D,
|
||||
definition: TerrainPropDefinition,
|
||||
) -> void:
|
||||
if not definition.has_collision():
|
||||
return
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "TrunkCollision"
|
||||
body.collision_layer = 1
|
||||
|
|
@ -373,44 +828,21 @@ func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
|
|||
prop.add_child(body)
|
||||
var collision := CollisionShape3D.new()
|
||||
collision.name = "CollisionShape"
|
||||
var shape := CylinderShape3D.new()
|
||||
shape.radius = 0.5 if broad_tree else 0.35
|
||||
shape.height = 4.2 if broad_tree else 4.8
|
||||
collision.position.y = shape.height * 0.5
|
||||
collision.shape = shape
|
||||
body.add_child(collision)
|
||||
|
||||
|
||||
func _cache_prop_source(source_name: String) -> void:
|
||||
var source_root := PROP_SOURCE.instantiate()
|
||||
var source := source_root.find_child(
|
||||
source_name,
|
||||
true,
|
||||
false,
|
||||
) as MeshInstance3D
|
||||
if source != null and source.mesh != null:
|
||||
_source_meshes[source_name] = source.mesh
|
||||
_source_bases[source_name] = source.transform.basis
|
||||
_source_minimum_y[source_name] = _minimum_transformed_mesh_y(
|
||||
source.mesh,
|
||||
source.transform.basis,
|
||||
)
|
||||
if definition.has_box_collision():
|
||||
var box := BoxShape3D.new()
|
||||
box.size = definition.collision_box_size
|
||||
collision.shape = box
|
||||
collision.position = definition.collision_offset
|
||||
else:
|
||||
push_warning("Generated terrain prop source '%s' is unavailable." % source_name)
|
||||
source_root.free()
|
||||
|
||||
|
||||
func _minimum_transformed_mesh_y(mesh: Mesh, basis: Basis) -> float:
|
||||
var bounds := mesh.get_aabb()
|
||||
var minimum_y := INF
|
||||
for corner_index: int in 8:
|
||||
var corner := bounds.position + Vector3(
|
||||
bounds.size.x if (corner_index & 1) != 0 else 0.0,
|
||||
bounds.size.y if (corner_index & 2) != 0 else 0.0,
|
||||
bounds.size.z if (corner_index & 4) != 0 else 0.0,
|
||||
var cylinder := CylinderShape3D.new()
|
||||
cylinder.radius = definition.collision_radius
|
||||
cylinder.height = definition.collision_height
|
||||
collision.shape = cylinder
|
||||
collision.position = (
|
||||
definition.collision_offset
|
||||
+ Vector3.UP * cylinder.height * 0.5
|
||||
)
|
||||
minimum_y = minf(minimum_y, (basis * corner).y)
|
||||
return minimum_y if minimum_y < INF else 0.0
|
||||
body.add_child(collision)
|
||||
|
||||
|
||||
func _build_shoreline_reference() -> void:
|
||||
|
|
@ -492,15 +924,6 @@ func _append_triangle(
|
|||
result.append(PackedVector3Array([a, b, c]))
|
||||
|
||||
|
||||
func _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||
var result: Array[MeshInstance3D] = []
|
||||
if root is MeshInstance3D:
|
||||
result.append(root as MeshInstance3D)
|
||||
for child: Node in root.get_children():
|
||||
result.append_array(_collect_mesh_instances(child))
|
||||
return result
|
||||
|
||||
|
||||
func _clear_children(root: Node) -> void:
|
||||
for child: Node in root.get_children():
|
||||
root.remove_child(child)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue