Merge branch 'upstream/development' into LYN-6769_TestingRPCs

This commit is contained in:
Gene Walters
2021-12-03 10:40:48 -08:00
894 changed files with 11820 additions and 7018 deletions
+124
View File
@@ -0,0 +1,124 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import os, traceback, binascii, sys, json, pathlib, logging
import azlmbr.math
import azlmbr.bus
from scene_helpers import *
#
# SceneAPI Processor
#
def update_manifest(scene):
import uuid
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
graph = sceneData.SceneGraph(scene.graph)
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
mesh_name_list.sort(key=lambda node: str.casefold(node.get_path()))
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
source_filename_only = os.path.basename(clean_filename)
created_entities = []
previous_entity_id = azlmbr.entity.InvalidEntityId
first_mesh = True
# Make a list of mesh node paths
mesh_path_list = list(map(lambda node: node.get_path(), mesh_name_list))
# Assume the first mesh is the main mesh
main_mesh = mesh_name_list[0]
mesh_path = main_mesh.get_path()
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, main_mesh.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
# Set our current node as the only node that is included in this MeshGroup
scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
# Explicitly remove all other nodes to prevent implicit inclusions
for node in mesh_path_list:
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
# Create a LOD rule
lod_rule = scene_manifest.mesh_group_add_lod_rule(mesh_group)
# Loop all the mesh nodes after the first
for x in mesh_path_list[1:]:
# Add a new LOD level
lod = scene_manifest.lod_rule_add_lod(lod_rule)
# Select the current mesh for this LOD level
scene_manifest.lod_select_node(lod, x)
# Unselect every other mesh for this LOD level
for y in mesh_path_list:
if y != x:
scene_manifest.lod_unselect_node(lod, y)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
# The assetHint will be converted to an AssetId later during prefab loading
json_update = json.dumps({
"Controller": { "Configuration": { "ModelAsset": {
"assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
});
# Apply the JSON above to the component we created
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
create_prefab(scene_manifest, source_filename_only, [entity_id])
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
log_exception_traceback()
except:
log_exception_traceback()
global sceneJobHandler
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene as sceneApi
if (sceneJobHandler == None):
sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
sceneJobHandler = None
@@ -0,0 +1,95 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import traceback, logging, json
from typing import Tuple, List
import azlmbr.bus
from scene_api import scene_data as sceneData
from scene_api.scene_data import SceneGraphName
def log_exception_traceback():
"""
Outputs an exception stacktrace.
"""
data = traceback.format_exc()
logger = logging.getLogger('python')
logger.error(data)
def sanitize_name_for_disk(name: str):
"""
Removes illegal filename characters from a string.
:param name: String to clean.
:return: Name with illegal characters removed.
"""
return "".join(char for char in name if char not in "|<>:\"/?*\\")
def get_mesh_node_names(scene_graph: sceneData.SceneGraph) -> Tuple[List[SceneGraphName], List[str]]:
"""
Returns a tuple of all the mesh nodes as well as all the node paths
:param scene_graph: Scene graph to search
:return: Tuple of [Mesh Nodes, All Node Paths]
"""
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
mesh_data_list = []
node = scene_graph.get_root()
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if scene_graph.has_node_child(node):
children.append(scene_graph.get_node_child(node))
node_name = sceneData.SceneGraphName(scene_graph.get_node_name(node))
paths.append(node_name.get_path())
# store any node that has mesh data content
node_content = scene_graph.get_node_content(node)
if node_content.CastWithTypeName('MeshData'):
if scene_graph.is_node_end_point(node) is False:
if len(node_name.get_path()):
mesh_data_list.append(sceneData.SceneGraphName(scene_graph.get_node_name(node)))
# advance to next node
if scene_graph.has_node_sibling(node):
node = scene_graph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return mesh_data_list, paths
def create_prefab(scene_manifest: sceneData.SceneManifest, prefab_name: str, entities: list) -> None:
prefab_filename = prefab_name + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", entities,
prefab_filename)
if created_template_id is None or created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output is not None and output.IsSuccess():
json_string = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
json_result = json.loads(json_string)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(prefab_name, uuid, json_result)
else:
raise RuntimeError(
"SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
@@ -5,55 +5,17 @@
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import os, traceback, binascii, sys, json, pathlib, logging
import azlmbr.math
import azlmbr.bus
import azlmbr.math
from scene_api.scene_data import PrimitiveShape, DecompositionMode
from scene_helpers import *
#
# SceneAPI Processor
#
def log_exception_traceback():
data = traceback.format_exc()
logger = logging.getLogger('python')
logger.error(data)
def get_mesh_node_names(sceneGraph):
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
meshDataList = []
node = sceneGraph.get_root()
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
# store any node that has mesh data content
nodeContent = sceneGraph.get_node_content(node)
if nodeContent.CastWithTypeName('MeshData'):
if sceneGraph.is_node_end_point(node) is False:
if (len(nodeName.get_path())):
meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node)))
# advance to next node
if sceneGraph.has_node_sibling(node):
node = sceneGraph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return meshDataList, paths
def add_material_component(entity_id):
# Create an override AZ::Render::EditorMaterialComponent
editor_material_component = azlmbr.entity.EntityUtilityBus(
@@ -64,24 +26,53 @@ def add_material_component(entity_id):
# this fills out the material asset to a known product AZMaterial asset relative path
json_update = json.dumps({
"Controller": { "Configuration": { "materials": [
{
"Key": {},
"Value": { "MaterialAsset":{
"assetHint": "materials/basic_grey.azmaterial"
}}
}]
}}
});
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_material_component, json_update)
"Controller": {"Configuration": {"materials": [
{
"Key": {},
"Value": {"MaterialAsset": {
"assetHint": "materials/basic_grey.azmaterial"
}}
}]
}}
})
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
editor_material_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
def add_physx_meshes(scene_manifest: sceneData.SceneManifest, source_file_name: str, mesh_name_list: List, all_node_paths: List[str]):
first_mesh = mesh_name_list[0].get_path()
# Add a Box Primitive PhysX mesh with a comment
physx_box = scene_manifest.add_physx_primitive_mesh_group(source_file_name + "_box", PrimitiveShape.BOX, 0.0, None)
scene_manifest.physx_mesh_group_add_comment(physx_box, "This is a box primitive")
# Select the first mesh, unselect every other node
scene_manifest.physx_mesh_group_add_selected_node(physx_box, first_mesh)
for node in all_node_paths:
if node != first_mesh:
scene_manifest.physx_mesh_group_add_unselected_node(physx_box, node)
# Add a Convex Mesh PhysX mesh with a comment
convex_mesh = scene_manifest.add_physx_convex_mesh_group(source_file_name + "_convex", 0.08, .0004,
True, True, True, True, True, 24, True, "Glass")
scene_manifest.physx_mesh_group_add_comment(convex_mesh, "This is a convex mesh")
# Select/Unselect nodes using lists
all_except_first_mesh = [x for x in all_node_paths if x != first_mesh]
scene_manifest.physx_mesh_group_add_selected_unselected_nodes(convex_mesh, [first_mesh], all_except_first_mesh)
# Configure mesh decomposition for this mesh
scene_manifest.physx_mesh_group_decompose_meshes(convex_mesh, 512, 32, .002, 100100, DecompositionMode.TETRAHEDRON,
0.06, 0.055, 0.00015, 3, 3, True, False)
# Add a Triangle mesh
triangle = scene_manifest.add_physx_triangle_mesh_group(source_file_name + "_triangle", False, True, True, True, True, True)
scene_manifest.physx_mesh_group_add_selected_unselected_nodes(triangle, [first_mesh], all_except_first_mesh)
def update_manifest(scene):
import json
import uuid, os
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
@@ -89,9 +80,9 @@ def update_manifest(scene):
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
@@ -101,6 +92,8 @@ def update_manifest(scene):
previous_entity_id = azlmbr.entity.InvalidEntityId
first_mesh = True
add_physx_meshes(scene_manifest, source_filename_only, mesh_name_list, all_node_paths)
# Loop every mesh node in the scene
for activeMeshIndex in range(len(mesh_name_list)):
mesh_name = mesh_name_list[activeMeshIndex]
@@ -108,7 +101,7 @@ def update_manifest(scene):
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
mesh_group_name = sanitize_name_for_disk(mesh_group_name)
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
@@ -129,37 +122,61 @@ def update_manifest(scene):
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
# The assetHint will be converted to an AssetId later during prefab loading
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just
# created + the azmodel extension The MeshGroup we created will be output as a product in the asset's path
# named mesh_group_name.azmodel The assetHint will be converted to an AssetId later during prefab loading
json_update = json.dumps({
"Controller": { "Configuration": { "ModelAsset": {
"assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
});
"Controller": {"Configuration": {"ModelAsset": {
"assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel"}}}
})
# Apply the JSON above to the component we created
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
editor_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
# Add a physics component referencing the triangle mesh we made for the first node
if previous_entity_id is None:
physx_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
entity_id, "{FD429282-A075-4966-857F-D0BBF186CFE6} EditorColliderComponent")
json_update = json.dumps({
"ShapeConfiguration": {
"PhysicsAsset": {
"Asset": {
"assetHint": os.path.join(source_relative_path, source_filename_only + "_triangle.pxmesh")
}
}
}
})
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, physx_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for PhysX mesh component")
# an example of adding a material component to override the default material
if previous_entity_id is not None and first_mesh:
first_mesh = False
add_material_component(entity_id)
# Get the transform component
transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
# Set this entity to be a child of the last entity we created
# This is just an example of how to do parenting and isn't necessarily useful to parent everything like this
if previous_entity_id is not None:
transform_json = json.dumps({
"Parent Entity" : previous_entity_id.to_json()
});
"Parent Entity": previous_entity_id.to_json()
})
# Apply the JSON update
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, transform_component, transform_json)
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
transform_component, transform_json)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Transform component")
@@ -171,37 +188,23 @@ def update_manifest(scene):
created_entities.append(entity_id)
# Create a prefab with all our entities
prefab_filename = source_filename_only + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", created_entities, prefab_filename)
if created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output.IsSuccess():
jsonString = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
jsonResult = json.loads(jsonString)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(source_filename_only, uuid, jsonResult)
else:
raise RuntimeError("SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
create_prefab(scene_manifest, source_filename_only, created_entities)
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
print(f'ERROR - {err}')
log_exception_traceback()
except:
log_exception_traceback()
@@ -209,10 +212,12 @@ def on_update_manifest(args):
global sceneJobHandler
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene as sceneApi
if (sceneJobHandler == None):
if sceneJobHandler is None:
sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
@@ -149,11 +149,14 @@ class AtomComponentProperties:
def display_mapper(property: str = 'name') -> str:
"""
Display Mapper component properties.
- 'LDR color Grading LUT' is the Low Definition Range (LDR) color grading for Look-up Textures (LUT) which is
an Asset.id value corresponding to a lighting asset file.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Display Mapper',
'LDR color Grading LUT': 'Controller|Configuration|LDR color Grading LUT',
}
return properties[property]
@@ -390,7 +393,7 @@ class AtomComponentProperties:
'name': 'PostFX Shape Weight Modifier',
'requires': [AtomComponentProperties.postfx_layer()],
'shapes': ['Axis Aligned Box Shape', 'Box Shape', 'Capsule Shape', 'Compound Shape', 'Cylinder Shape',
'Disk Shape', 'Polygon Prism Shape', 'Quad Shape', 'Sphere Shape', 'Vegetation Reference Shape'],
'Disk Shape', 'Polygon Prism Shape', 'Quad Shape', 'Sphere Shape', 'Shape Reference'],
}
return properties[property]
@@ -5,6 +5,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class Tests:
camera_creation = (
"Camera Entity successfully created",
@@ -39,6 +40,9 @@ class Tests:
is_hidden = (
"Entity is hidden",
"Entity was not hidden")
ldr_color_grading_lut = (
"LDR color Grading LUT asset set",
"LDR color Grading LUT asset could not be set")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
@@ -71,16 +75,19 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
5) Enter/Exit game mode.
6) Test IsHidden.
7) Test IsVisible.
8) Delete Display Mapper entity.
9) UNDO deletion.
10) REDO deletion.
11) Look for errors and asserts.
8) Set LDR color Grading LUT asset.
9) Delete Display Mapper entity.
10) UNDO deletion.
11) REDO deletion.
12) Look for errors and asserts.
:return: None
"""
import os
import azlmbr.legacy.general as general
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
@@ -97,7 +104,7 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
Report.critical_result(Tests.display_mapper_creation, display_mapper_entity.exists())
# 2. Add Display Mapper component to Display Mapper entity.
display_mapper_entity.add_component(AtomComponentProperties.display_mapper())
display_mapper_component = display_mapper_entity.add_component(AtomComponentProperties.display_mapper())
Report.critical_result(
Tests.display_mapper_component,
display_mapper_entity.has_component(AtomComponentProperties.display_mapper()))
@@ -140,19 +147,29 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
general.idle_wait_frames(1)
Report.result(Tests.is_visible, display_mapper_entity.is_visible() is True)
# 8. Delete Display Mapper entity.
# 8. Set LDR color Grading LUT asset.
display_mapper_asset_path = os.path.join("TestData", "test.lightingpreset.azasset")
display_mapper_asset = Asset.find_asset_by_path(display_mapper_asset_path, False)
display_mapper_component.set_component_property_value(
AtomComponentProperties.display_mapper("LDR color Grading LUT"), display_mapper_asset.id)
Report.result(
Tests.ldr_color_grading_lut,
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper("LDR color Grading LUT")) == display_mapper_asset.id)
# 9. Delete Display Mapper entity.
display_mapper_entity.delete()
Report.result(Tests.entity_deleted, not display_mapper_entity.exists())
# 9. UNDO deletion.
# 10. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, display_mapper_entity.exists())
# 10. REDO deletion.
# 11. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not display_mapper_entity.exists())
# 11. Look for errors and asserts.
# 12. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
@@ -34,6 +34,38 @@ class TestHelper:
# JIRA: SPEC-2880
# general.idle_wait_frames(1)
@staticmethod
def create_level(level_name: str) -> bool:
"""
:param level_name: The name of the level to be created
:return: True if ECreateLevelResult returns 0, False otherwise with logging to report reason
"""
Report.info(f"Creating level {level_name}")
# Use these hardcoded values to pass expected values for old terrain system until new create_level API is
# available
heightmap_resolution = 1024
heightmap_meters_per_pixel = 1
terrain_texture_resolution = 4096
use_terrain = False
result = general.create_level_no_prompt(level_name, heightmap_resolution, heightmap_meters_per_pixel,
terrain_texture_resolution, use_terrain)
# Result codes are ECreateLevelResult defined in CryEdit.h
if result == 1:
Report.info(f"{level_name} level already exists")
elif result == 2:
Report.info("Failed to create directory")
elif result == 3:
Report.info("Directory length is too long")
elif result != 0:
Report.info("Unknown error, failed to create level")
else:
Report.info(f"{level_name} level created successfully")
return result == 0
@staticmethod
def open_level(directory : str, level : str):
# type: (str, str) -> None
@@ -0,0 +1,144 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class HeightTests:
single_gradient_height_correct = (
"Successfully retrieved height for gradient1.",
"Failed to retrieve height for gradient1."
)
double_gradient_height_correct = (
"Successfully retrieved height when two gradients exist.",
"Failed to retrieve height when two gradients exist."
)
triple_gradient_height_correct = (
"Successfully retrieved height when three gradients exist.",
"Failed to retrieve height when three gradients exist."
)
terrain_data_changed_call_count_correct = (
"OnTerrainDataChanged called expected number of times.",
"OnTerrainDataChanged call count incorrect."
)
def TerrainHeightGradientList_AddRemoveGradientWorks():
"""
Summary:
Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree.
:return: None
"""
import os
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.terrain as terrain
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.entity as EntityId
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.editor_entity_utils import EditorEntity
terrain_changed_call_count = 0
expected_terrain_changed_calls = 0
aabb_component_name = "Axis Aligned Box Shape"
gradientlist_component_name = "Terrain Height Gradient List"
layerspawner_component_name = "Terrain Layer Spawner"
gradient_value_path = "Configuration|Value"
def create_entity_at(entity_name, components_to_add, x, y, z):
entity = hydra.Entity(entity_name)
entity.create_entity(math.Vector3(x, y, z), components_to_add)
return entity
def on_terrain_changed(args):
nonlocal terrain_changed_call_count
terrain_changed_call_count += 1
def set_component_path_val(entity, component, path, value):
entity.get_set_test(component, path, value)
def set_gradients_check_height(main_entity, gradient_list, expected_height, test_results):
nonlocal expected_terrain_changed_calls
test_tolerance = 0.01
gradient_list_path = "Configuration|Gradient Entities"
set_component_path_val(main_entity, 1, gradient_list_path, gradient_list)
expected_terrain_changed_calls += 1
# Wait until the terrain data has been updated.
helper.wait_for_condition(lambda: terrain_changed_call_count == expected_terrain_changed_calls, 2.0)
# Get the height at the origin.
height = terrain.TerrainDataRequestBus(bus.Broadcast, "GetHeightFromFloats", 0.0, 0.0, 0)
Report.result(test_results, sys_math.isclose(height, expected_height, abs_tol=test_tolerance))
helper.init_idle()
# Open a level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
general.idle_wait_frames(1)
# Add a terrain world component
world_component = hydra.add_level_component("Terrain World")
aabb_height = 1024.0
box_dimensions = math.Vector3(1.0, 1.0, aabb_height);
# Create a main entity with a LayerSpawner, AAbb and HeightGradientList.
main_entity = create_entity_at("entity2", [layerspawner_component_name, gradientlist_component_name, aabb_component_name], 0.0, 0.0, aabb_height/2.0)
# Create three gradient entities.
gradient_entity1 = create_entity_at("Constant Gradient1", ["Constant Gradient"], 0.0, 0.0, 0.0);
gradient_entity2 = create_entity_at("Constant Gradient2", ["Constant Gradient"], 0.0, 0.0, 0.0);
gradient_entity3 = create_entity_at("Constant Gradient3", ["Constant Gradient"], 0.0, 0.0, 0.0);
# Give everything a chance to finish initializing.
general.idle_wait_frames(1)
# Set the gradients to different values.
gradient_values = [0.5, 0.8, 0.3]
set_component_path_val(gradient_entity1, 0, gradient_value_path, gradient_values[0])
set_component_path_val(gradient_entity2, 0, gradient_value_path, gradient_values[1])
set_component_path_val(gradient_entity3, 0, gradient_value_path, gradient_values[2])
# Give the TerrainSystem time to tick.
general.idle_wait_frames(1)
# Set the dimensions of the Aabb.
set_component_path_val(main_entity, 2, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
# Set up a handler to wait for notifications from the TerrainSystem.
handler = azlmbr.terrain.TerrainDataNotificationBusHandler()
handler.connect()
handler.add_callback("OnTerrainDataChanged", on_terrain_changed)
# Add a gradient to GradientList, then check the height returned from the TerrainSystem is correct.
set_gradients_check_height(main_entity, [gradient_entity1.id], aabb_height * gradient_values[0], HeightTests.single_gradient_height_correct)
# Add gradient2 and check height at the origin, this should have changed to match the second gradient value.
set_gradients_check_height(main_entity, [gradient_entity1.id, gradient_entity2.id], aabb_height * gradient_values[1], HeightTests.double_gradient_height_correct)
# Add gradient3, the height should still be the second value, as that was the highest.
set_gradients_check_height(main_entity, [gradient_entity1.id, gradient_entity2.id, gradient_entity3.id], aabb_height * gradient_values[1], HeightTests.triple_gradient_height_correct)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainHeightGradientList_AddRemoveGradientWorks)
@@ -72,7 +72,7 @@ def Terrain_SupportsPhysics():
# 2) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
entity1_components_to_add = ["Axis Aligned Box Shape", "Terrain Layer Spawner", "Terrain Height Gradient List", "Terrain Physics Heightfield Collider", "PhysX Heightfield Collider"]
entity2_components_to_add = ["Vegetation Reference Shape", "Gradient Transform Modifier", "FastNoise Gradient"]
entity2_components_to_add = ["Shape Reference", "Gradient Transform Modifier", "FastNoise Gradient"]
ball_components_to_add = ["Sphere Shape", "PhysX Collider", "PhysX Rigid Body"]
terrain_spawner_entity = hydra.Entity("TestEntity1")
terrain_spawner_entity.create_entity(azmath.Vector3(512.0, 512.0, 50.0), entity1_components_to_add)
@@ -27,3 +27,6 @@ class TestAutomation(EditorTestSuite):
class test_Terrain_SupportsPhysics(EditorSharedTest):
from .EditorScripts import Terrain_SupportsPhysics as test_module
class test_TerrainHeightGradientList_AddRemoveGradientWorks(EditorSharedTest):
from .EditorScripts import TerrainHeightGradientList_AddRemoveGradientWorks as test_module
@@ -15,6 +15,7 @@ import logging
# Import LyTestTools
import ly_test_tools.o3de.asset_processor as asset_processor_commands
import ly_test_tools.o3de.asset_processor_utils
logger = logging.getLogger(__name__)
@@ -36,5 +37,8 @@ def asset_processor(request: pytest.fixture, workspace: pytest.fixture) -> asset
ap.stop()
request.addfinalizer(teardown)
for n in ly_test_tools.o3de.asset_processor_utils.processList:
assert not ly_test_tools.o3de.asset_processor_utils.check_ap_running(n), f"{n} process did not shutdown correctly."
return ap
@@ -72,9 +72,9 @@ def DynamicSliceInstanceSpawner_Embedded_E2E():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(512.0, 480.0, 38.0)
# 2) Create a new entity with required vegetation area components and Script Canvas component for launcher test
@@ -73,9 +73,9 @@ def DynamicSliceInstanceSpawner_External_E2E():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(512.0, 480.0, 38.0)
# 2) Create a new entity with required vegetation area components and switch the Vegetation Asset List Source
@@ -76,9 +76,9 @@ def LayerBlender_E2E_Editor():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(500.49, 498.69, 46.66)
general.set_current_view_rotation(-42.05, 0.00, -36.33)
@@ -78,7 +78,7 @@ def LayerSpawner_InheritBehaviorFlag():
# Create Vegetation area and assign a valid asset
veg_1 = hydra.Entity("veg_1")
veg_1.create_entity(
position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"]
position, ["Vegetation Layer Spawner", "Shape Reference", "Vegetation Asset List"]
)
set_dynamic_slice_asset(veg_1, 2, os.path.join("Slices", "PinkFlower.dynamicslice"))
veg_1.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id)
@@ -86,7 +86,7 @@ def LayerSpawner_InheritBehaviorFlag():
# Create second vegetation area and assign a valid asset
veg_2 = hydra.Entity("veg_2")
veg_2.create_entity(
position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"]
position, ["Vegetation Layer Spawner", "Shape Reference", "Vegetation Asset List"]
)
set_dynamic_slice_asset(veg_2, 2, os.path.join("Slices", "PurpleFlower.dynamicslice"))
veg_2.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id)
@@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
def LayerSpawner_InstancesPlantInAllSupportedShapes():
"""
Summary:
The level is loaded and vegetation area is created. Then the Vegetation Reference Shape
The level is loaded and vegetation area is created. Then the Shape Reference
component of vegetation area is pinned with entities of different shape components to check
if the vegetation plants in different shaped areas.
@@ -67,7 +67,7 @@ def LayerSpawner_InstancesPlantInAllSupportedShapes():
10.0, 10.0, 10.0,
asset_path)
vegetation.remove_component("Box Shape")
vegetation.add_component("Vegetation Reference Shape")
vegetation.add_component("Shape Reference")
# Create surface for planting on
dynveg.create_surface_entity("Surface Entity", entity_position, 60.0, 60.0, 1.0)
@@ -18,6 +18,17 @@ from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, E
class TestAutomation(EditorTestSuite):
enable_prefab_system = False
# Helpers for test asset cleanup
def cleanup_test_level(self, workspace):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
def cleanup_test_slices(self, workspace):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_1.slice")], True, True)
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_2.slice")], True, True)
class test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(EditorParallelTest):
from .EditorScripts import DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks as test_module
@@ -37,10 +48,7 @@ class TestAutomation(EditorTestSuite):
class test_SpawnerSlices_SliceCreationAndVisibilityToggleWorks(EditorSingleTest):
# Custom teardown to remove slice asset created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_1.slice")], True, True)
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_2.slice")], True, True)
TestAutomation.cleanup_test_slices(self, workspace)
from .EditorScripts import SpawnerSlices_SliceCreationAndVisibilityToggleWorks as test_module
class test_AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(EditorParallelTest):
@@ -148,29 +156,32 @@ class TestAutomation(EditorTestSuite):
class test_SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes(EditorParallelTest):
from .EditorScripts import SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope as test_module
@pytest.mark.xfail(reason="Intermittently fails to create level")
class test_DynamicSliceInstanceSpawner_Embedded_E2E_Editor(EditorSingleTest):
from .EditorScripts import DynamicSliceInstanceSpawner_Embedded_E2E as test_module
# Custom teardown to remove test level created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
@pytest.mark.xfail(reason="Intermittently fails to create level")
class test_DynamicSliceInstanceSpawner_External_E2E_Editor(EditorSingleTest):
from .EditorScripts import DynamicSliceInstanceSpawner_External_E2E as test_module
# Custom teardown to remove test level created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
@pytest.mark.xfail(reason="Intermittently fails to create level")
class test_LayerBlender_E2E_Editor(EditorSingleTest):
from .EditorScripts import LayerBlender_E2E_Editor as test_module
# Custom teardown to remove test level created during test
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
TestAutomation.cleanup_test_level(self, workspace)
@@ -96,7 +96,7 @@ def AreaNodes_DependentComponentsAdded():
'SpawnerAreaNode': [
'Vegetation Layer Spawner',
'Vegetation Asset List',
'Vegetation Reference Shape'
'Shape Reference'
],
'MeshBlockerAreaNode': [
'Vegetation Layer Blocker (Mesh)',
@@ -104,7 +104,7 @@ def AreaNodes_DependentComponentsAdded():
],
'BlockerAreaNode': [
'Vegetation Layer Blocker',
'Vegetation Reference Shape'
'Shape Reference'
]
}
@@ -82,7 +82,7 @@ def Edit_DisabledNodeDuplication():
nodes = {
'SpawnerAreaNode': 'Vegetation Asset List',
'MeshBlockerAreaNode': 'Mesh',
'BlockerAreaNode': 'Vegetation Reference Shape',
'BlockerAreaNode': 'Shape Reference',
'FastNoiseGradientNode': 'Gradient Transform Modifier',
'ImageGradientNode': 'Gradient Transform Modifier',
'PerlinNoiseGradientNode': 'Gradient Transform Modifier',
@@ -104,7 +104,7 @@ def GradientNodes_DependentComponentsAdded():
# we will be checking for
commonComponents = [
'Gradient Transform Modifier',
'Vegetation Reference Shape'
'Shape Reference'
]
componentNames = []
for name in gradients:
@@ -114,7 +114,7 @@ def GradientNodes_DependentComponentsAdded():
# Create nodes for the gradients that have additional required dependencies and check if
# the Entity created by adding the node has the appropriate Component and required
# Gradient Transform Modifier and Vegetation Reference Shape components added automatically to it
# Gradient Transform Modifier and Shape Reference components added automatically to it
newGraph = graph.GraphManagerRequestBus(bus.Broadcast, 'GetGraph', newGraphId)
x = 10.0
y = 10.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
0,0,0,0,0,0
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e169277bca473325281d5fe043cffc9196bd3ef46f6bffbea6e0b5e3b7194a1
size 62700
@@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Editor/Scripts/auto_lod.py"
}
]
}