Add seasonal tree palettes

This commit is contained in:
Alexander Sellite 2026-09-02 17:20:25 -04:00
parent 1163cef320
commit 23cf53fa14
35 changed files with 861 additions and 43 deletions

View file

@ -21,12 +21,16 @@ const Gatherables: GatherableCatalog = preload(
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const CalendarSeasonType = preload("res://world/calendar_season.gd")
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
&"foliage_wind_source_materials"
)
const PROP_PRIMARY_MATERIAL_ROLL_META: StringName = (
&"terrain_prop_primary_material_roll"
)
const FIRST_SEED := 13001
const SECOND_SEED := 13002
const RIVER_FALLBACK_SEED := 13012
@ -81,6 +85,7 @@ func _run() -> void:
configured_generator.elevated_cliff_third_tier_base_chance = 0.0
configured_generator.elevated_cliff_third_tier_stack_chance = 1.0
configured_generator.elevated_cliff_double_third_tier_chance = 0.0
region.set_calendar_season(CalendarSeasonType.Season.FALL)
root.add_child(region)
await process_frame
assert(region.generate_world(FIRST_SEED))
@ -757,24 +762,23 @@ func _validate_generated_region(
float(child.get_meta(&"terrain_prop_visual_scale", 1.0))
)
tree_yaws[snappedf((child as Node3D).rotation.y, 0.01)] = true
if prop_id in [
&"prop_tree_1",
&"prop_tree_2",
&"prop_tree_3",
&"prop_tree_large",
]:
if not definition.variant_material_slot_names.is_empty():
assert(
_has_material_variant_override(
child.get_node_or_null("Visual"),
definition.material_variants,
definition.primary_material_variants_for_season(
CalendarSeasonType.Season.FALL
),
)
)
if not definition.secondary_material_variants.is_empty():
assert(
_has_material_variant_override(
child.get_node_or_null("Visual"),
definition.secondary_material_variants,
)
)
_validate_seasonal_foliage_switch(region, decorations)
assert(not palm_clusters.is_empty())
for members: Array in palm_clusters.values():
assert(members.size() >= 1 and members.size() <= 3)
@ -1169,17 +1173,33 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
assert(tree.material_variants.size() >= 3)
assert(not tree.variant_material_slot_names.is_empty())
assert(tree.summer_material_variants.size() == 3)
assert(tree.fall_material_variants.size() == 4)
assert(
tree.primary_material_variants_for_season(
CalendarSeasonType.Season.SPRING
) == tree.summer_material_variants
)
assert(
tree.primary_material_variants_for_season(
CalendarSeasonType.Season.WINTER
) == tree.summer_material_variants
)
assert(tree.secondary_material_variants.size() >= 3)
assert(not tree.secondary_variant_material_slot_names.is_empty())
var pine := catalog.definition_for_id(&"prop_pine")
assert(pine != null)
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
assert(pine.material_variants.is_empty())
assert(pine.summer_material_variants.size() == 1)
assert(pine.fall_material_variants.size() == 1)
assert(not pine.has_gatherable_surface())
var large_pine := catalog.definition_for_id(&"prop_pine_large")
assert(large_pine != null)
assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale)
assert(large_pine.material_variants.is_empty())
assert(large_pine.summer_material_variants.size() == 1)
assert(large_pine.fall_material_variants.size() == 1)
assert(not large_pine.has_gatherable_surface())
var palm := catalog.definition_for_id(&"prop_palm")
assert(palm != null)
@ -1189,6 +1209,8 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
assert(palm.maximum_cluster_size == 3)
assert(palm.minimum_cluster_radius > palm.clearance_radius)
assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius)
assert(palm.summer_material_variants.size() == 1)
assert(palm.fall_material_variants.size() == 1)
assert(palm.prefer_ocean_facing)
assert(not palm.local_overhang_direction.is_zero_approx())
var aligned_tree_collisions: Dictionary[StringName, Vector2] = {
@ -1594,6 +1616,82 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
return null
func _validate_seasonal_foliage_switch(
region: GeneratedWorldRegion,
decorations: Node3D,
) -> void:
var original_signature := _decoration_signature(region)
var expected_rolls: Dictionary[String, float] = {}
var found_broadleaf := false
var found_pine := false
var found_palm := false
for prop_node: Node in decorations.get_children():
var fall_prop := prop_node as Node3D
var prop_id := StringName(fall_prop.get_meta(&"terrain_prop_id", &""))
var fall_definition := region.get_prop_catalog().definition_for_id(prop_id)
if (
fall_definition == null
or fall_definition.variant_material_slot_names.is_empty()
):
continue
assert(fall_prop.has_meta(PROP_PRIMARY_MATERIAL_ROLL_META))
var roll := float(fall_prop.get_meta(PROP_PRIMARY_MATERIAL_ROLL_META))
assert(roll >= 0.0 and roll < 1.0)
expected_rolls[fall_prop.name] = roll
assert(_has_material_variant_override(
fall_prop.get_node_or_null("Visual"),
fall_definition.primary_material_variants_for_season(
CalendarSeasonType.Season.FALL
),
))
found_broadleaf = found_broadleaf or String(prop_id).begins_with(
"prop_tree_"
)
found_pine = found_pine or prop_id in [
&"prop_pine",
&"prop_pine_large",
]
found_palm = found_palm or prop_id == &"prop_palm"
assert(found_broadleaf and found_pine and found_palm)
region.set_calendar_season(CalendarSeasonType.Season.SUMMER)
assert(_decoration_signature(region) == original_signature)
for prop_node: Node in decorations.get_children():
var summer_prop := prop_node as Node3D
if not expected_rolls.has(summer_prop.name):
continue
assert(is_equal_approx(
float(summer_prop.get_meta(PROP_PRIMARY_MATERIAL_ROLL_META)),
expected_rolls[summer_prop.name],
))
var summer_definition := region.get_prop_catalog().definition_for_id(
StringName(summer_prop.get_meta(&"terrain_prop_id", &""))
)
assert(_has_material_variant_override(
summer_prop.get_node_or_null("Visual"),
summer_definition.primary_material_variants_for_season(
CalendarSeasonType.Season.SUMMER
),
))
assert(_first_foliage_wind_material(decorations) != null)
region.set_calendar_season(CalendarSeasonType.Season.FALL)
assert(_decoration_signature(region) == original_signature)
for prop_node: Node in decorations.get_children():
var restored_prop := prop_node as Node3D
if not expected_rolls.has(restored_prop.name):
continue
var restored_definition := region.get_prop_catalog().definition_for_id(
StringName(restored_prop.get_meta(&"terrain_prop_id", &""))
)
assert(_has_material_variant_override(
restored_prop.get_node_or_null("Visual"),
restored_definition.primary_material_variants_for_season(
CalendarSeasonType.Season.FALL
),
))
func _first_foliage_wind_material(root_node: Node) -> ShaderMaterial:
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
@ -1652,8 +1750,15 @@ func _has_material_variant_override(
surface_index,
null,
) as Material
if source_material != null and source_material in variants:
return true
var source_standard := source_material as StandardMaterial3D
if source_standard != null and source_material in variants:
var visible_color: Color = wind_material.get_shader_parameter(
"albedo_color"
)
if visible_color.is_equal_approx(
source_standard.albedo_color
):
return true
for child: Node in root_node.get_children():
if _has_material_variant_override(child, variants):
return true

View file

@ -0,0 +1,274 @@
extends SceneTree
const CalendarSeasonType = preload("res://world/calendar_season.gd")
const PropCatalog: TerrainPropCatalog = preload(
"res://world/generation/props/terrain_prop_catalog.tres"
)
const RegionScene: PackedScene = preload(
"res://world/generation/generated_world_region.tscn"
)
const BROADLEAF_IDS: Array[StringName] = [
&"prop_tree_1",
&"prop_tree_2",
&"prop_tree_3",
&"prop_tree_large",
]
const IMPORTED_FOLIAGE_NAMES: Dictionary = {
&"prop_tree_1": &"leaf_autumn_light",
&"prop_tree_2": &"leaf_autumn",
&"prop_tree_3": &"leaf_autumn_dark",
&"prop_tree_large": &"leaf_autumn_mid",
&"prop_pine": &"pine_summer",
&"prop_pine_large": &"pine_autumn",
&"prop_palm": &"palm_autumn",
}
const PROP_PRIMARY_MATERIAL_ROLL_META: StringName = (
&"terrain_prop_primary_material_roll"
)
const PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META: StringName = (
&"terrain_prop_legacy_primary_material_index"
)
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
assert(PropCatalog.validation_errors().is_empty())
for stable_id: StringName in BROADLEAF_IDS:
var definition := PropCatalog.definition_for_id(stable_id)
assert(definition != null)
_assert_palette(
definition.summer_material_variants,
PackedStringArray([
"leaf_variant_light",
"leaf_variant_mid",
"leaf_variant_dark",
]),
[
Color(0.3387566, 0.7186728, 0, 1),
Color(0.3146831, 0.4631305, 0, 1),
Color(0.1712407, 0.2520192, 0, 1),
],
)
_assert_palette(
definition.fall_material_variants,
PackedStringArray([
"leaf_autumn_light",
"leaf_autumn",
"leaf_autumn_mid",
"leaf_autumn_dark",
]),
[
Color(0.8222013, 0.794216, 0, 1),
Color(0.6186816, 0.4523746, 0, 1),
Color(0.6841143, 0.3515543, 0, 1),
Color(0.3853541, 0.00134692, 0, 1),
],
)
assert(
definition.primary_material_variants_for_season(
CalendarSeasonType.Season.SPRING
) == definition.summer_material_variants
)
assert(
definition.primary_material_variants_for_season(
CalendarSeasonType.Season.WINTER
) == definition.summer_material_variants
)
_assert_single_seasonal_tree(
&"prop_pine",
&"pine_summer",
Color(0.02171588, 0.2242201, 0.1360403, 1),
&"pine_autumn",
Color(0.02388079, 0.1285609, 0, 1),
)
_assert_single_seasonal_tree(
&"prop_pine_large",
&"pine_summer",
Color(0.02171588, 0.2242201, 0.1360403, 1),
&"pine_autumn",
Color(0.02388079, 0.1285609, 0, 1),
)
_assert_single_seasonal_tree(
&"prop_palm",
&"palm_summer",
Color(0.3146831, 0.4631305, 0, 1),
&"palm_autumn",
Color(0.2951757, 0.3575695, 0.162893, 1),
)
for stable_id: StringName in IMPORTED_FOLIAGE_NAMES:
var definition := PropCatalog.definition_for_id(stable_id)
var instance := definition.packed_scene.instantiate()
var material_names := PackedStringArray()
_collect_material_names(instance, material_names)
assert(IMPORTED_FOLIAGE_NAMES[stable_id] in material_names)
instance.free()
var region := RegionScene.instantiate() as GeneratedWorldRegion
assert(region != null)
assert(bool(region.call(
"_material_name_matches_targets",
&"leaf_autumn_mid",
PackedStringArray(["leaf"]),
)))
assert(bool(region.call(
"_material_name_matches_targets",
&"pine_winter",
PackedStringArray(["pine"]),
)))
assert(bool(region.call(
"_material_name_matches_targets",
&"palm_spring",
PackedStringArray(["palm"]),
)))
_validate_palette_roll_mapping(region)
_validate_primary_roll_random_state(region)
region.free()
print("Terrain seasonal material validation: PASS")
quit()
func _assert_single_seasonal_tree(
stable_id: StringName,
summer_name: StringName,
summer_color: Color,
fall_name: StringName,
fall_color: Color,
) -> void:
var definition := PropCatalog.definition_for_id(stable_id)
assert(definition != null)
_assert_palette(
definition.summer_material_variants,
PackedStringArray([summer_name]),
[summer_color],
)
_assert_palette(
definition.fall_material_variants,
PackedStringArray([fall_name]),
[fall_color],
)
func _assert_palette(
materials: Array[Material],
expected_names: PackedStringArray,
expected_colors: Array[Color],
) -> void:
assert(materials.size() == expected_names.size())
assert(materials.size() == expected_colors.size())
for index: int in materials.size():
var material := materials[index] as StandardMaterial3D
assert(material != null)
assert(material.resource_name == expected_names[index])
assert(material.albedo_color.is_equal_approx(expected_colors[index]))
func _validate_palette_roll_mapping(region: GeneratedWorldRegion) -> void:
var definition := PropCatalog.definition_for_id(&"prop_tree_1")
assert(definition != null)
var instance := definition.packed_scene.instantiate()
var palette := definition.fall_material_variants
for index: int in palette.size():
var roll := (float(index) + 0.5) / float(palette.size())
region.call(
"_apply_material_variant_for_roll",
instance,
definition.variant_material_slot_names,
palette,
roll,
)
assert(_has_surface_override(instance, palette[index]))
instance.free()
func _validate_primary_roll_random_state(
region: GeneratedWorldRegion,
) -> void:
var broadleaf := PropCatalog.definition_for_id(&"prop_tree_1")
var baseline := RandomNumberGenerator.new()
baseline.seed = 29092026
var actual := RandomNumberGenerator.new()
actual.seed = baseline.seed
var legacy_index := baseline.randi_range(
0,
broadleaf.material_variants.size() - 1,
)
var selection := region.call(
"_primary_material_selection",
broadleaf,
actual,
) as Dictionary
var roll := float(selection.get("roll", -1.0))
assert(roll >= 0.0 and roll < 1.0)
assert(int(selection.get("legacy_index", -1)) == legacy_index)
assert(actual.state == baseline.state)
var prop := Node3D.new()
prop.set_meta(PROP_PRIMARY_MATERIAL_ROLL_META, 0.125)
prop.set_meta(
PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META,
legacy_index,
)
var summer_roll := float(region.call(
"_primary_material_roll_for_palette",
prop,
broadleaf,
broadleaf.summer_material_variants,
))
assert(
floori(summer_roll * broadleaf.summer_material_variants.size())
== legacy_index
)
var fall_roll := float(region.call(
"_primary_material_roll_for_palette",
prop,
broadleaf,
broadleaf.fall_material_variants,
))
assert(is_equal_approx(fall_roll, 0.125))
prop.free()
var pine := PropCatalog.definition_for_id(&"prop_pine")
var pine_random := RandomNumberGenerator.new()
pine_random.seed = 29092026
var pine_initial_state := pine_random.state
var pine_selection := region.call(
"_primary_material_selection",
pine,
pine_random,
) as Dictionary
assert(int(pine_selection.get("legacy_index", -2)) == -1)
assert(pine_random.state == pine_initial_state)
func _has_surface_override(root_node: Node, expected: Material) -> bool:
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():
if (
mesh_instance.get_surface_override_material(surface_index)
== expected
):
return true
for child: Node in root_node.get_children():
if _has_surface_override(child, expected):
return true
return false
func _collect_material_names(
node: Node,
material_names: PackedStringArray,
) -> void:
var mesh_instance := node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.mesh.surface_get_material(surface_index)
if material != null:
material_names.append(material.resource_name)
for child: Node in node.get_children():
_collect_material_names(child, material_names)

View file

@ -0,0 +1 @@
uid://dicorkv8nbkb5

View file

@ -4,6 +4,7 @@ const TestWorldScene: PackedScene = preload("res://world/test_world.tscn")
const SetupPageScene: PackedScene = preload(
"res://ui/new_game_setup_page.tscn"
)
const CalendarSeasonType = preload("res://world/calendar_season.gd")
func _initialize() -> void:
@ -84,6 +85,7 @@ func _validate_world_switching() -> void:
root.add_child(world)
await process_frame
assert(world.get_world_layout() == WorldLayout.GENERATED)
world.set_calendar_season(CalendarSeasonType.Season.FALL)
world.set_foliage_wind_strength_multiplier(2.0)
world.set_water_motion_strength_multiplier(2.0)
var initial_generator := world.get_node(
@ -134,6 +136,15 @@ func _validate_world_switching() -> void:
"Regions/GeneratedWorldRegion"
) as GeneratedWorldRegion
assert(generated_region != null)
assert(
int(generated_region.get("_calendar_season"))
== CalendarSeasonType.Season.FALL
)
world.set_calendar_season(CalendarSeasonType.Season.SUMMER)
assert(
int(generated_region.get("_calendar_season"))
== CalendarSeasonType.Season.SUMMER
)
assert(is_equal_approx(
float(generated_region.get("_foliage_wind_strength_multiplier")), 2.0
))