Expand generated terrain and forest variation
This commit is contained in:
parent
779e3d983e
commit
b2752b1074
25 changed files with 898 additions and 145 deletions
|
|
@ -6,9 +6,9 @@
|
|||
[sub_resource type="Resource" id="Rule_sand_tree"]
|
||||
script = ExtResource("2_rule")
|
||||
procedural_group = &"sand_tree"
|
||||
placement_chance = 4200
|
||||
adjacency_bonus = 1200
|
||||
maximum_density = 5000
|
||||
placement_chance = 2200
|
||||
adjacency_bonus = 900
|
||||
maximum_density = 2800
|
||||
minimum_placements = 1
|
||||
allowed_prop_ids = PackedStringArray("prop_palm")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
|||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||
allowed_prop_ids = PackedStringArray("prop_tree_large", "prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
script = ExtResource("2_rule")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
|||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
allowed_prop_ids = PackedStringArray("prop_pine")
|
||||
allowed_prop_ids = PackedStringArray("prop_pine_large", "prop_pine")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
script = ExtResource("2_rule")
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const OCEAN_POOL: FishPool = preload(
|
|||
const WATER_HEIGHT := -0.25
|
||||
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_CHANCE_SCALE := 10000
|
||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||
|
|
@ -268,6 +269,7 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
record.get("position", Vector3.ZERO),
|
||||
coordinate,
|
||||
biome.stable_id,
|
||||
int(record.get("ocean_facing_edges", 0)),
|
||||
random,
|
||||
terrain_triangles,
|
||||
):
|
||||
|
|
@ -445,6 +447,7 @@ func _maybe_add_prop(
|
|||
chunk_center: Vector3,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
ocean_facing_edges: int,
|
||||
random: RandomNumberGenerator,
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
|
|
@ -460,6 +463,7 @@ func _maybe_add_prop(
|
|||
chunk_center,
|
||||
chunk_coordinate,
|
||||
biome_id,
|
||||
ocean_facing_edges,
|
||||
random,
|
||||
terrain_triangles,
|
||||
)
|
||||
|
|
@ -470,19 +474,26 @@ func _add_prop(
|
|||
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 visual_scale := random.randf_range(
|
||||
definition.minimum_visual_scale,
|
||||
definition.maximum_visual_scale,
|
||||
var cluster_size := random.randi_range(
|
||||
definition.minimum_cluster_size,
|
||||
definition.maximum_cluster_size,
|
||||
)
|
||||
var scaled_clearance := definition.clearance_radius * visual_scale
|
||||
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 placement := Vector3.ZERO
|
||||
var found_surface := false
|
||||
for attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||
var offset := Vector3(
|
||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||
0.0,
|
||||
|
|
@ -507,6 +518,118 @@ func _add_prop(
|
|||
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
|
||||
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 _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:
|
||||
|
|
@ -522,8 +645,10 @@ func _add_prop(
|
|||
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 = random.randf_range(-PI, PI)
|
||||
prop.rotation.y = yaw
|
||||
_decorations.add_child(prop)
|
||||
visual_root.name = "Visual"
|
||||
prop.add_child(visual_root)
|
||||
|
|
@ -533,7 +658,9 @@ func _add_prop(
|
|||
_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(scaled_clearance)
|
||||
_placed_prop_clearance_radii.append(
|
||||
definition.clearance_radius * visual_scale
|
||||
)
|
||||
_placed_prop_groups.append(definition.procedural_group)
|
||||
if definition.gatherable_anchor_height > 0.0:
|
||||
var anchor := Marker3D.new()
|
||||
|
|
@ -547,6 +674,46 @@ func _add_prop(
|
|||
return true
|
||||
|
||||
|
||||
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.")
|
||||
|
|
@ -776,6 +943,7 @@ func _ensure_minimum_props(
|
|||
record.get("position", Vector3.ZERO),
|
||||
coordinate,
|
||||
biome.stable_id,
|
||||
int(record.get("ocean_facing_edges", 0)),
|
||||
random,
|
||||
terrain_triangles,
|
||||
):
|
||||
|
|
@ -819,17 +987,32 @@ func _apply_prop_material_variant(
|
|||
definition: TerrainPropDefinition,
|
||||
random: RandomNumberGenerator,
|
||||
) -> void:
|
||||
if (
|
||||
definition.material_variants.is_empty()
|
||||
or definition.variant_material_slot_names.is_empty()
|
||||
):
|
||||
return
|
||||
var variant := definition.material_variants[
|
||||
random.randi_range(0, definition.material_variants.size() - 1)
|
||||
]
|
||||
_apply_material_variant_to_meshes(
|
||||
_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,
|
||||
)
|
||||
|
||||
|
|
|
|||
BIN
world/generation/props/assets/prop_pine_large.glb
Normal file
BIN
world/generation/props/assets/prop_pine_large.glb
Normal file
Binary file not shown.
45
world/generation/props/assets/prop_pine_large.glb.import
Normal file
45
world/generation/props/assets/prop_pine_large.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bsislktcsck1y"
|
||||
path="res://.godot/imported/prop_pine_large.glb-97f0e7e162a6a624faaf2cc023211d3e.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://world/generation/props/assets/prop_pine_large.glb"
|
||||
dest_files=["res://.godot/imported/prop_pine_large.glb-97f0e7e162a6a624faaf2cc023211d3e.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
BIN
world/generation/props/assets/prop_tree_large.glb
Normal file
BIN
world/generation/props/assets/prop_tree_large.glb
Normal file
Binary file not shown.
45
world/generation/props/assets/prop_tree_large.glb.import
Normal file
45
world/generation/props/assets/prop_tree_large.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://c46jcb2vo2fd"
|
||||
path="res://.godot/imported/prop_tree_large.glb-0a4ab94061e4876be025fa3d63aa7bd8.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://world/generation/props/assets/prop_tree_large.glb"
|
||||
dest_files=["res://.godot/imported/prop_tree_large.glb-0a4ab94061e4876be025fa3d63aa7bd8.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
|
|
@ -11,6 +11,15 @@ packed_scene = ExtResource("2_scene")
|
|||
procedural_group = &"sand_tree"
|
||||
required_chunk_tags = PackedStringArray("sand")
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 2.0
|
||||
clearance_radius = 0.65
|
||||
minimum_visual_scale = 0.5
|
||||
maximum_visual_scale = 1.0
|
||||
minimum_cluster_size = 2
|
||||
maximum_cluster_size = 3
|
||||
minimum_cluster_radius = 1.4
|
||||
maximum_cluster_radius = 3.0
|
||||
prefer_ocean_facing = true
|
||||
local_overhang_direction = Vector2(-0.883, 0.469)
|
||||
ocean_facing_spread_degrees = 55.0
|
||||
collision_radius = 0.4
|
||||
collision_height = 6.0
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_pine"
|
||||
label = "pine"
|
||||
label = "small pine"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 0.7
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.55
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.22
|
||||
minimum_visual_scale = 0.65
|
||||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.5
|
||||
collision_height = 4.0
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
|
|||
20
world/generation/props/definitions/prop_pine_large.tres
Normal file
20
world/generation/props/definitions/prop_pine_large.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_pine_large.glb" id="2_scene"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_pine_large"
|
||||
label = "large pine"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 3.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.65
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.65
|
||||
collision_height = 9.5
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_1"
|
||||
label = "small tree"
|
||||
label = "small deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.3
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.15
|
||||
minimum_visual_scale = 0.65
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_2"
|
||||
label = "medium tree"
|
||||
label = "medium deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.5
|
||||
minimum_visual_scale = 0.8
|
||||
maximum_visual_scale = 1.18
|
||||
minimum_visual_scale = 0.7
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_3"
|
||||
label = "large tree"
|
||||
label = "decorative deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.8
|
||||
minimum_visual_scale = 0.85
|
||||
maximum_visual_scale = 1.15
|
||||
minimum_visual_scale = 0.7
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
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
|
||||
|
|
|
|||
30
world/generation/props/definitions/prop_tree_large.tres
Normal file
30
world/generation/props/definitions/prop_tree_large.tres
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_large.glb" id="2_scene"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_large"
|
||||
label = "large deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 12.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 2.5
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
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
|
||||
9
world/generation/props/materials/wood_dark.tres
Normal file
9
world/generation/props/materials/wood_dark.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_dark"
|
||||
albedo_color = Color(0.163, 0.139, 0.0254, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
9
world/generation/props/materials/wood_light.tres
Normal file
9
world/generation/props/materials/wood_light.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_light"
|
||||
albedo_color = Color(0.644, 0.563, 0.356, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
9
world/generation/props/materials/wood_mid.tres
Normal file
9
world/generation/props/materials/wood_mid.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_mid"
|
||||
albedo_color = Color(0.333, 0.271, 0.114, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=16 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=18 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_catalog.gd" id="1_catalog"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_bridge.tres" id="2_bridge"]
|
||||
|
|
@ -15,7 +15,9 @@
|
|||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_2.tres" id="13_tree_2"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_3.tres" id="14_tree_3"]
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="15_definition_script"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_large.tres" id="16_tree_large"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_pine_large.tres" id="17_pine_large"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_catalog")
|
||||
definitions = Array[ExtResource("15_definition_script")]([ExtResource("2_bridge"), ExtResource("3_dock"), ExtResource("4_log"), ExtResource("5_post_1"), ExtResource("6_post_2"), ExtResource("7_mushroom"), ExtResource("8_palm"), ExtResource("9_pine"), ExtResource("10_timber_1"), ExtResource("11_timber_2"), ExtResource("12_tree_1"), ExtResource("13_tree_2"), ExtResource("14_tree_3")])
|
||||
definitions = Array[ExtResource("15_definition_script")]([ExtResource("2_bridge"), ExtResource("3_dock"), ExtResource("4_log"), ExtResource("5_post_1"), ExtResource("6_post_2"), ExtResource("7_mushroom"), ExtResource("8_palm"), ExtResource("9_pine"), ExtResource("10_timber_1"), ExtResource("11_timber_2"), ExtResource("12_tree_1"), ExtResource("13_tree_2"), ExtResource("14_tree_3"), ExtResource("16_tree_large"), ExtResource("17_pine_large")])
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ const MAX_PACKED_SOLVER_VARIANTS := 62
|
|||
@export var elevated_cliff_edge_chunk_id: StringName = &"chunk_0011"
|
||||
@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012"
|
||||
@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 0.75
|
||||
## A smaller third tier reuses the existing corner geometry at the authored
|
||||
## two-meter level interval. It is nested into the lower cliff assembly rather
|
||||
## than occupying or replacing additional terrain-grid cells.
|
||||
@export_range(0.0, 1.0, 0.05) var elevated_cliff_third_level_chance := 0.7
|
||||
@export_range(0.1, 10.0, 0.1) var elevated_cliff_level_height := 2.0
|
||||
@export_group("")
|
||||
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
|
||||
@export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6
|
||||
|
|
@ -68,6 +73,7 @@ var _generated_chunks: Node3D
|
|||
var _backtrack_count := 0
|
||||
var _elevated_feature_center := Vector2i(-1, -1)
|
||||
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
||||
var _stacked_elevated_placements: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -100,6 +106,9 @@ func generate() -> bool:
|
|||
if not _apply_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
if not _configure_stacked_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
var solution_root := _build_solution_root()
|
||||
if solution_root == null:
|
||||
_assign_placements(previous_placements)
|
||||
|
|
@ -138,6 +147,9 @@ func generate_from_placement_keys(keys: PackedStringArray) -> bool:
|
|||
previous_placements.assign(_placements)
|
||||
_assign_placements(resolved)
|
||||
_backtrack_count = 0
|
||||
if not _configure_stacked_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
var solution_root := _build_solution_root()
|
||||
if solution_root == null:
|
||||
_assign_placements(previous_placements)
|
||||
|
|
@ -1184,6 +1196,7 @@ func _candidate_can_occupy_cell(
|
|||
func _prepare_elevated_feature_region() -> bool:
|
||||
_elevated_feature_center = Vector2i(-1, -1)
|
||||
_elevated_feature_reserved_grass_indices.clear()
|
||||
_stacked_elevated_placements.clear()
|
||||
if not elevated_cliff_feature_enabled:
|
||||
_build_grid_solver_caches()
|
||||
return true
|
||||
|
|
@ -1439,6 +1452,65 @@ func _apply_elevated_feature() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _configure_stacked_elevated_feature() -> bool:
|
||||
_stacked_elevated_placements.clear()
|
||||
if (
|
||||
not elevated_cliff_feature_enabled
|
||||
or elevated_cliff_third_level_chance <= 0.0
|
||||
):
|
||||
return true
|
||||
var top_coordinate := Vector2i(-1, -1)
|
||||
for index: int in _placements.size():
|
||||
var placement := _placements[index]
|
||||
if (
|
||||
placement != null
|
||||
and placement.definition.stable_id == elevated_cliff_top_chunk_id
|
||||
):
|
||||
top_coordinate = Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
break
|
||||
if top_coordinate.x < 0:
|
||||
return true
|
||||
var stack_random := RandomNumberGenerator.new()
|
||||
stack_random.seed = generation_seed ^ 0x7312DC11F
|
||||
if stack_random.randf() >= elevated_cliff_third_level_chance:
|
||||
return true
|
||||
var corner_definition := catalog.definition_for_id(
|
||||
elevated_cliff_corner_chunk_id
|
||||
)
|
||||
if corner_definition == null:
|
||||
push_error(
|
||||
"Stacked cliff feature references missing corner chunk %s."
|
||||
% elevated_cliff_corner_chunk_id
|
||||
)
|
||||
return false
|
||||
# Four nearly half-cell-offset corners form a closed 2x2 ring. A slight
|
||||
# inward overlap keeps the authored curved feet fully seated on the lower
|
||||
# plateau instead of exposing a hairline gap at its outermost vertices.
|
||||
var specs: Array[Dictionary] = [
|
||||
{"offset": Vector2(-0.45, -0.45), "turns": 2},
|
||||
{"offset": Vector2(0.45, -0.45), "turns": 1},
|
||||
{"offset": Vector2(-0.45, 0.45), "turns": 3},
|
||||
{"offset": Vector2(0.45, 0.45), "turns": 0},
|
||||
]
|
||||
for spec: Dictionary in specs:
|
||||
var variant := _authored_variant(
|
||||
corner_definition,
|
||||
int(spec["turns"]),
|
||||
)
|
||||
if variant == null:
|
||||
push_error(
|
||||
"Stacked cliff feature cannot resolve %s rotation %d."
|
||||
% [elevated_cliff_corner_chunk_id, spec["turns"]]
|
||||
)
|
||||
return false
|
||||
_stacked_elevated_placements.append({
|
||||
"support_coordinate": top_coordinate,
|
||||
"offset": spec["offset"],
|
||||
"variant": variant,
|
||||
})
|
||||
return true
|
||||
|
||||
|
||||
func _variant_respects_ocean_boundary(
|
||||
variant: TerrainChunkVariant,
|
||||
coordinate: Vector2i,
|
||||
|
|
@ -2040,9 +2112,74 @@ func _build_solution_root() -> Node3D:
|
|||
_add_collision(chunk_root, variant.definition)
|
||||
if show_chunk_labels:
|
||||
_add_chunk_label(chunk_root, variant)
|
||||
if not _add_stacked_elevated_chunks(solution_root):
|
||||
solution_root.free()
|
||||
return null
|
||||
return solution_root
|
||||
|
||||
|
||||
func _add_stacked_elevated_chunks(solution_root: Node3D) -> bool:
|
||||
for index: int in _stacked_elevated_placements.size():
|
||||
var record := _stacked_elevated_placements[index]
|
||||
var variant := record.get("variant") as TerrainChunkVariant
|
||||
var support_coordinate: Vector2i = record.get(
|
||||
"support_coordinate",
|
||||
Vector2i(-1, -1),
|
||||
)
|
||||
var support_root := _find_chunk_root_for_coordinate(
|
||||
solution_root,
|
||||
support_coordinate,
|
||||
)
|
||||
if variant == null or support_root == null:
|
||||
push_error("Stacked cliff feature lost its supporting terrain cell.")
|
||||
return false
|
||||
var stacked_root := variant.definition.packed_scene.instantiate() as Node3D
|
||||
if stacked_root == null:
|
||||
push_error(
|
||||
"%s does not instantiate as a stacked Node3D."
|
||||
% variant.stable_key()
|
||||
)
|
||||
return false
|
||||
stacked_root.name = "Stacked_%s_%d" % [
|
||||
variant.stable_key().replace("@", "r"),
|
||||
index,
|
||||
]
|
||||
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
||||
stacked_root.position = Vector3(
|
||||
offset.x * catalog.chunk_size,
|
||||
elevated_cliff_level_height,
|
||||
offset.y * catalog.chunk_size,
|
||||
)
|
||||
stacked_root.rotation.y = variant.rotation_radians()
|
||||
stacked_root.set_meta(&"terrain_stacked_elevation", true)
|
||||
stacked_root.set_meta(
|
||||
&"terrain_chunk_id",
|
||||
variant.definition.stable_id,
|
||||
)
|
||||
stacked_root.set_meta(
|
||||
&"terrain_chunk_coordinate",
|
||||
support_coordinate,
|
||||
)
|
||||
support_root.add_child(stacked_root)
|
||||
if build_collision:
|
||||
_add_collision(stacked_root, variant.definition)
|
||||
if show_chunk_labels:
|
||||
_add_chunk_label(stacked_root, variant)
|
||||
return true
|
||||
|
||||
|
||||
func _find_chunk_root_for_coordinate(
|
||||
solution_root: Node3D,
|
||||
coordinate: Vector2i,
|
||||
) -> Node3D:
|
||||
for child: Node in solution_root.get_children():
|
||||
if child.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)) == (
|
||||
coordinate
|
||||
):
|
||||
return child as Node3D
|
||||
return null
|
||||
|
||||
|
||||
func _instantiate_chunk_root(
|
||||
definition: TerrainChunkDefinition,
|
||||
) -> Node3D:
|
||||
|
|
@ -2171,6 +2308,7 @@ func _build_summary() -> Dictionary:
|
|||
"counts": counts,
|
||||
"variant_counts": variant_counts,
|
||||
"placements": placement_keys(),
|
||||
"stacked_elevated_placements": stacked_elevated_placement_keys(),
|
||||
"layout_fingerprint": placement_fingerprint(),
|
||||
}
|
||||
|
||||
|
|
@ -2214,9 +2352,29 @@ func placement_fingerprint() -> String:
|
|||
"chunk_size:%.6f" % (catalog.chunk_size if catalog != null else 0.0),
|
||||
])
|
||||
fingerprint_input.append_array(placement_keys())
|
||||
fingerprint_input.append_array(stacked_elevated_placement_keys())
|
||||
return "\n".join(fingerprint_input).sha256_text()
|
||||
|
||||
|
||||
func stacked_elevated_placement_keys() -> PackedStringArray:
|
||||
var result := PackedStringArray()
|
||||
for record: Dictionary in _stacked_elevated_placements:
|
||||
var variant := record.get("variant") as TerrainChunkVariant
|
||||
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
||||
if variant == null:
|
||||
continue
|
||||
result.append(
|
||||
"%s@%.2f,%.2f,+%.2fm"
|
||||
% [
|
||||
variant.stable_key(),
|
||||
offset.x,
|
||||
offset.y,
|
||||
elevated_cliff_level_height,
|
||||
]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func placement_records() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for index: int in _placements.size():
|
||||
|
|
@ -2276,4 +2434,18 @@ func get_primary_terrain_meshes() -> Array[MeshInstance3D]:
|
|||
)
|
||||
if primary_mesh != null and primary_mesh.mesh != null:
|
||||
result.append(primary_mesh)
|
||||
for child: Node in chunk_root.get_children():
|
||||
if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
|
||||
continue
|
||||
var definition := catalog.definition_for_id(
|
||||
StringName(child.get_meta(&"terrain_chunk_id", &""))
|
||||
)
|
||||
if definition == null:
|
||||
continue
|
||||
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
child,
|
||||
definition.primary_mesh_name,
|
||||
)
|
||||
if stacked_mesh != null and stacked_mesh.mesh != null:
|
||||
result.append(stacked_mesh)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -87,6 +87,23 @@ func validation_errors() -> PackedStringArray:
|
|||
"%s has an inverted visual scale range."
|
||||
% definition.stable_id
|
||||
)
|
||||
if definition.minimum_cluster_size > definition.maximum_cluster_size:
|
||||
errors.append(
|
||||
"%s has an inverted cluster size range."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.maximum_cluster_size > 1
|
||||
and (
|
||||
definition.minimum_cluster_radius <= 0.0
|
||||
or definition.minimum_cluster_radius
|
||||
> definition.maximum_cluster_radius
|
||||
)
|
||||
):
|
||||
errors.append(
|
||||
"%s has an invalid loose-cluster radius."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
not definition.material_variants.is_empty()
|
||||
and definition.variant_material_slot_names.is_empty()
|
||||
|
|
@ -101,4 +118,26 @@ func validation_errors() -> PackedStringArray:
|
|||
"%s contains an empty material variant."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
not definition.secondary_material_variants.is_empty()
|
||||
and definition.secondary_variant_material_slot_names.is_empty()
|
||||
):
|
||||
errors.append(
|
||||
"%s has secondary variants but no target material slots."
|
||||
% definition.stable_id
|
||||
)
|
||||
for material: Material in definition.secondary_material_variants:
|
||||
if material == null:
|
||||
errors.append(
|
||||
"%s contains an empty secondary material variant."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.prefer_ocean_facing
|
||||
and definition.local_overhang_direction.is_zero_approx()
|
||||
):
|
||||
errors.append(
|
||||
"%s prefers the ocean but has no local overhang direction."
|
||||
% definition.stable_id
|
||||
)
|
||||
return errors
|
||||
|
|
|
|||
|
|
@ -21,12 +21,28 @@ extends Resource
|
|||
## Procedural instances use a deterministic uniform scale in this range.
|
||||
@export_range(0.1, 4.0, 0.05) var minimum_visual_scale := 1.0
|
||||
@export_range(0.1, 4.0, 0.05) var maximum_visual_scale := 1.0
|
||||
## Values above one create a loose group of separately anchored instances.
|
||||
## Cluster members are distributed across the configured scale range so a
|
||||
## three-member cluster naturally contains a small, medium, and large prop.
|
||||
@export_range(1, 4, 1) var minimum_cluster_size := 1
|
||||
@export_range(1, 4, 1) var maximum_cluster_size := 1
|
||||
@export_range(0.0, 10.0, 0.05) var minimum_cluster_radius := 0.0
|
||||
@export_range(0.0, 10.0, 0.05) var maximum_cluster_radius := 0.0
|
||||
## Presentation-only offset. The prop root remains exactly terrain-aligned.
|
||||
@export var visual_offset := Vector3.ZERO
|
||||
## A single material is chosen per prop and applied only to matching imported
|
||||
## material slots. This keeps trunks untouched while varying foliage.
|
||||
@export var variant_material_slot_names := PackedStringArray()
|
||||
@export var material_variants: Array[Material] = []
|
||||
## An independent second material channel, used when both foliage and wood
|
||||
## should vary without coupling their color choices.
|
||||
@export var secondary_variant_material_slot_names := PackedStringArray()
|
||||
@export var secondary_material_variants: Array[Material] = []
|
||||
## Some asymmetric props can loosely face an authored ocean edge. The local
|
||||
## direction describes the model's natural overhang in Godot X/Z space.
|
||||
@export var prefer_ocean_facing := false
|
||||
@export var local_overhang_direction := Vector2(0.0, -1.0)
|
||||
@export_range(0.0, 180.0, 1.0) var ocean_facing_spread_degrees := 45.0
|
||||
@export_range(0.0, 5.0, 0.05) var collision_radius := 0.0
|
||||
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0
|
||||
@export var collision_box_size := Vector3.ZERO
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue