TerrainSystem_VegetationSpawnsOnTerrainSurfaces.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. class VegetationTests:
  7. vegetation_on_gradient_1 = (
  8. "Vegetation detected at correct position on Gradient1",
  9. "Vegetation not detected at correct position on Gradient1"
  10. )
  11. vegetation_on_gradient_2 = (
  12. "Vegetation detected at correct position on Gradient2",
  13. "Vegetation not detected at correct position on Gradient2"
  14. )
  15. unfiltered_vegetation_count_correct = (
  16. "Unfiltered vegetation spawn count correct",
  17. "Unfiltered vegetation spawn count incorrect"
  18. )
  19. testTag2_excluded_vegetation_count_correct = (
  20. "TestTag2 filtered vegetation count correct",
  21. "TestTag2 filtered vegetation count incorrect"
  22. )
  23. testTag2_excluded_vegetation_z_correct = (
  24. "TestTag2 filtered vegetation spawned in correct position",
  25. "TestTag2 filtered vegetation failed to spawn in correct position"
  26. )
  27. testTag3_excluded_vegetation_count_correct = (
  28. "TestTag3 filtered vegetation count correct",
  29. "TestTag3 filtered vegetation count incorrect"
  30. )
  31. testTag3_excluded_vegetation_z_correct = (
  32. "TestTag3 filtered vegetation spawned in correct position",
  33. "TestTag3 filtered vegetation failed to spawn in correct position"
  34. )
  35. cleared_exclusion_vegetation_count_correct = (
  36. "Cleared filter vegetation count correct",
  37. "Cleared filter vegetation count incorrect"
  38. )
  39. def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
  40. """
  41. Summary:
  42. Load an empty level,
  43. Create two entities with constant gradient components with different values.
  44. Create two non-overlapping entities with TerrainLayerSpawners in adjacent 20 m x 20 m boxes.
  45. Each spawner has a different constant height.
  46. Create an entity to spawn vegetation in a 20 m x 20 m boxes where 10 m overlaps the first spawner, and 10 m overlaps the second.
  47. Ensure that vegetation spawns at the correct heights.
  48. Add a VegetationSurfaceMaskFilter and ensure it responds correctly to surface changes.
  49. :return: None
  50. """
  51. import os
  52. import math as sys_math
  53. import azlmbr.legacy.general as general
  54. import azlmbr.bus as bus
  55. import azlmbr.math as math
  56. import azlmbr.areasystem as areasystem
  57. import azlmbr.vegetation as vegetation
  58. import azlmbr.terrain as terrain
  59. import azlmbr.surface_data as surface_data
  60. import editor_python_test_tools.hydra_editor_utils as hydra
  61. from editor_python_test_tools.utils import Report
  62. from editor_python_test_tools.utils import TestHelper as helper
  63. def create_entity_at(entity_name, components_to_add, x, y, z):
  64. entity = hydra.Entity(entity_name)
  65. entity.create_entity(math.Vector3(x, y, z), components_to_add)
  66. return entity
  67. def FindHighestAndLowestZValuesInArea(aabb):
  68. vegetation_items = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', aabb)
  69. lowest_z = min([item.position.z for item in vegetation_items])
  70. highest_z = max([item.position.z for item in vegetation_items])
  71. return highest_z, lowest_z
  72. # Open an empty level.
  73. hydra.open_base_level()
  74. general.idle_wait_frames(1)
  75. box_height = 20.0
  76. box_y_position = 10.0
  77. box_dimensions = math.Vector3(20.0, 20.0, box_height)
  78. # Add Terrain Rendering
  79. hydra.add_level_component("Terrain World")
  80. hydra.add_level_component("Terrain World Renderer")
  81. # Create two terrain entities at adjoining positions
  82. terrain_entity_1 = create_entity_at("Terrain1", ["Terrain Layer Spawner", "Axis Aligned Box Shape", "Terrain Height Gradient List", "Terrain Surface Gradient List"], 0.0, box_y_position, box_height/2.0)
  83. terrain_entity_1.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
  84. terrain_entity_2 = create_entity_at("Terrain2", ["Terrain Layer Spawner", "Axis Aligned Box Shape", "Terrain Height Gradient List", "Terrain Surface Gradient List"], 20.0, box_y_position, box_height/2.0)
  85. terrain_entity_2.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
  86. # Create two gradient entities.
  87. gradient_value_1 = 0.25
  88. gradient_value_2 = 0.5
  89. gradient_entity_1 = create_entity_at("Gradient1", ["Constant Gradient"], 0.0, 0.0, 0.0)
  90. gradient_entity_1.get_set_test(0, "Configuration|Value", gradient_value_1)
  91. gradient_entity_2 = create_entity_at("Gradient2", ["Constant Gradient"], 0.0, 0.0, 0.0)
  92. gradient_entity_2.get_set_test(0, "Configuration|Value", gradient_value_2)
  93. mapping = terrain.TerrainSurfaceGradientMapping()
  94. mapping.gradientEntityId = gradient_entity_1.id
  95. pte = hydra.get_property_tree(terrain_entity_1.components[3])
  96. pte.add_container_item("Configuration|Gradient to Surface Mappings", 0, mapping)
  97. mapping = terrain.TerrainSurfaceGradientMapping()
  98. mapping.gradientEntityId = gradient_entity_2.id
  99. pte = hydra.get_property_tree(terrain_entity_2.components[3])
  100. pte.add_container_item("Configuration|Gradient to Surface Mappings", 0, mapping)
  101. # create a vegetation entity that overlaps both terrain entities.
  102. vegetation_entity = create_entity_at("Vegetation", ["Vegetation Layer Spawner", "Axis Aligned Box Shape", "Vegetation Asset List", "Vegetation Surface Mask Filter"], 10.0, box_y_position, box_height/2.0)
  103. vegetation_entity.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
  104. # Set the vegetation area to a PrefabInstanceSpawner with a specific prefab asset selected.
  105. prefab_spawner = vegetation.PrefabInstanceSpawner()
  106. prefab_spawner.SetPrefabAssetPath(os.path.join("Prefabs", "PinkFlower.spawnable"))
  107. descriptor = hydra.get_component_property_value(vegetation_entity.components[2], 'Configuration|Embedded Assets|[0]')
  108. descriptor.spawner = prefab_spawner
  109. vegetation_entity.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor)
  110. # Assign gradients to layer spawners.
  111. terrain_entity_1.get_set_test(2, "Configuration|Gradient Entities", [gradient_entity_1.id])
  112. terrain_entity_2.get_set_test(2, "Configuration|Gradient Entities", [gradient_entity_2.id])
  113. # Move view so that the entities are visible.
  114. general.set_current_view_position(17.0, -66.0, 41.0)
  115. general.set_current_view_rotation(-15.0, 0.0, 0.0)
  116. # Expected item counts under conditions to be tested.
  117. # By default, vegetation spawns at a density of 20 items per 16 meters, so in a 20m square, there should
  118. # be (20 m * (20/16)) ^ 2 , or 25 ^ 2 instances. However, there's a final spawn point that lands directly on the max edge
  119. # of the 20 m box so vegetation actually spawns 26 ^ 2 = 676 instances. This is how many instances we expect with no filtering.
  120. expected_no_filtering_item_count = 676
  121. # When filtering to the 'terrain' tag, any points that fall on non-existent terrain (a hole) get filtered out.
  122. # The terrain excludes the max edges of the AABB for a spawner, so points that fall on or interpolate to the max edge will get
  123. # filtered out. The setup for this level is two adjacent terrain spawners that cover an area of (-10, 0) to (30, 20),
  124. # and the vegetation area covers (0, 0) to (20, 20). Any points that fall on max edge (20) in the Y direction get filtered out
  125. # because that's the max edge of the terrain, but points on the max edge (20) in the X direction do NOT get filtered out because
  126. # there is still more terrain past that point. Therefore, instead of 26 * 26 instances, we have 26 * 25 = 650 instances.
  127. expected_terrain_included_item_count = 650
  128. # When filtering to the 'test_tag2' tag, we're only keeping points in the vegetation area of (0, 0) to (20, 20) that come from the
  129. # first terrain spawner, which is (-10, 0) to (10, 20). The overlap box is (0, 0) to (10, 20), which contains
  130. # (10 m * (20/16)) = 12.5 points in X, but since we can't have half points, it only contains 12 points
  131. # In Y, we have (20 m * (20/16)) = 25 points. Both X and Y would have an additional point on the max boundary, but since the terrain
  132. # spawner ends on both max boundaries, those are excluded and aren't a part of the spawner's surface points.
  133. # So the expected count is 12 * 25 = 300 instances.
  134. expected_tag2_included_item_count = 300
  135. # When filtering to the 'test_tag3' tag, we're only keeping points in the vegetation area of (0, 0) to (20, 20) that come from the
  136. # second terrain spawner, which is (10, 0) to (30, 20). The overlap box is (10, 0) to (20, 20), which again contains
  137. # 12.5 points in X and 25 points in Y. Due to where the boundaries start, the half point *is* contained in this box, so
  138. # X has 13 points. Also, the max X boundary isn't the max spawner boundary, so it also has a 14th point. Y is still 25 points.
  139. # The expected count is 14 * 25 = 350 instances.
  140. expected_tag3_included_item_count = 350
  141. # Wait for the vegetation to spawn
  142. helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_no_filtering_item_count, 5.0)
  143. # Check the spawn count is correct.
  144. item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
  145. Report.result(VegetationTests.unfiltered_vegetation_count_correct, item_count == expected_no_filtering_item_count)
  146. test_aabb = math.Aabb_CreateFromMinMax(math.Vector3(-10.0, -10.0, 0.0), math.Vector3(30.0, 10.0, box_height))
  147. # Find the z positions of the items with the lowest and highest x values, this will avoid the overlap area where z values are blended between the surface heights.
  148. highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
  149. # Check that the z values are as expected.
  150. Report.result(VegetationTests.vegetation_on_gradient_1, sys_math.isclose(lowest_z, box_height * gradient_value_1, abs_tol=0.01))
  151. Report.result(VegetationTests.vegetation_on_gradient_2, sys_math.isclose(highest_z, box_height * gradient_value_2, abs_tol=0.01))
  152. # Assign SurfaceTags to the SurfaceGradientLists
  153. terrain_entity_1.get_set_test(3, "Configuration|Gradient to Surface Mappings|[0]|Surface Tag", surface_data.SurfaceTag("test_tag2"))
  154. terrain_entity_2.get_set_test(3, "Configuration|Gradient to Surface Mappings|[0]|Surface Tag", surface_data.SurfaceTag("test_tag3"))
  155. # Give the VegetationSurfaceFilter an inclusion list, set it to include test_tag3 which should
  156. # include only the instances on the upper terrain_entity_2.
  157. vegetation_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags", [surface_data.SurfaceTag()])
  158. vegetation_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags|[0]", surface_data.SurfaceTag("test_tag3"))
  159. # Wait for the vegetation to respawn and check z values.
  160. helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_tag3_included_item_count, 5.0)
  161. item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
  162. Report.result(VegetationTests.testTag2_excluded_vegetation_count_correct, item_count == expected_tag3_included_item_count)
  163. Report.info(f"Found item count for testTag3 inclusion: {item_count}")
  164. highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
  165. Report.result(VegetationTests.testTag2_excluded_vegetation_z_correct, lowest_z > box_height * gradient_value_1)
  166. # Set the filter to 'terrain' and ensure that vegetation spawns across both layers
  167. vegetation_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags|[0]", surface_data.SurfaceTag("terrain"))
  168. helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_terrain_included_item_count, 5.0)
  169. item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
  170. Report.result(VegetationTests.cleared_exclusion_vegetation_count_correct, item_count == expected_terrain_included_item_count)
  171. Report.info(f"Found item count for cleared exclusion: {item_count}")
  172. # Include test_tag2 so that only the instances on the lower terrain_entity_1 should be spawned.
  173. vegetation_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags|[0]", surface_data.SurfaceTag("test_tag2"))
  174. helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_tag2_included_item_count, 5.0)
  175. item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
  176. Report.result(VegetationTests.testTag3_excluded_vegetation_count_correct, item_count == expected_tag2_included_item_count)
  177. Report.info(f"Found item count for testTag2 inclusion: {item_count}")
  178. highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
  179. Report.result(VegetationTests.testTag3_excluded_vegetation_z_correct, highest_z < box_height * gradient_value_2)
  180. if __name__ == "__main__":
  181. from editor_python_test_tools.utils import Report
  182. Report.start_test(TerrainSystem_VegetationSpawnsOnTerrainSurfaces)