Add seasonal tree palettes
This commit is contained in:
parent
1163cef320
commit
23cf53fa14
35 changed files with 861 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
274
tests/terrain_seasonal_material_validation.gd
Normal file
274
tests/terrain_seasonal_material_validation.gd
Normal 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)
|
||||
1
tests/terrain_seasonal_material_validation.gd.uid
Normal file
1
tests/terrain_seasonal_material_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dicorkv8nbkb5
|
||||
|
|
@ -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
|
||||
))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue