Explorar o código

{lyn2264} Adding Python API for Actor Group (#6064)

* {lyn2264} Adding Python API for Actor Group

* updating the IBoneData with behavior attributes
* adding actor_group.py for making actor group JSON data
* adding physics_data.py for making physics JSON data

Signed-off-by: Allen Jackson <[email protected]>

* fixing dictionary issues

Signed-off-by: Allen Jackson <[email protected]>

* updated comment style to numpydoc

Signed-off-by: Allen Jackson <[email protected]>

* updated to numpydoc style

Signed-off-by: Allen Jackson <[email protected]>

* example of actor group rules

Signed-off-by: Allen Jackson <[email protected]>

* Updated based on feedback

Signed-off-by: Allen Jackson <[email protected]>
Allen Jackson %!s(int64=3) %!d(string=hai) anos
pai
achega
dae25beef5

+ 3 - 0
AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27f87510ae07771dbad3e430e31502b1d1d13dee868f143b7f1de2a7febc8eb9
+size 7046400

+ 8 - 0
AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx.assetinfo

@@ -0,0 +1,8 @@
+{
+    "values": [
+        {
+            "$type": "ScriptProcessorRule",
+            "scriptFilename": "Assets/TestAnim/scene_export_actor.py"
+        }
+    ]
+}

+ 221 - 0
AutomatedTesting/Assets/TestAnim/scene_export_actor.py

@@ -0,0 +1,221 @@
+#
+# 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, sys, uuid, os, json
+
+#
+# Example for exporting ActorGroup scene rules
+#
+
+def log_exception_traceback():
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    data = traceback.format_exception(exc_type, exc_value, exc_tb)
+    print(str(data))
+
+def get_node_names(sceneGraph, nodeTypeName, testEndPoint = False, validList = None):
+    import azlmbr.scene.graph
+    import scene_api.scene_data
+
+    node = sceneGraph.get_root()
+    nodeList = []
+    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 = scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node))
+        paths.append(nodeName.get_path())
+        
+        include = True 
+        
+        if (validList is not None):
+            include = False # if a valid list filter provided, assume to not include node name
+            name_parts = nodeName.get_path().split('.')
+            for valid in validList:
+                if (valid in name_parts[-1]):
+                    include = True
+                    break
+
+        # store any node that has provides specifc data content
+        nodeContent = sceneGraph.get_node_content(node)
+        if include and nodeContent.CastWithTypeName(nodeTypeName):
+            if testEndPoint is not None:
+                include = sceneGraph.is_node_end_point(node) is testEndPoint
+            if include:
+                if (len(nodeName.get_path())):
+                    nodeList.append(scene_api.scene_data.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 nodeList, paths
+
+def generate_mesh_group(scene, sceneManifest, meshDataList, paths):
+    # Compute the name of the scene file
+    clean_filename = scene.sourceFilename.replace('.', '_')
+    mesh_group_name = os.path.basename(clean_filename)
+
+    # make the mesh group
+    mesh_group = sceneManifest.add_mesh_group(mesh_group_name)
+    mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, clean_filename)) + '}'
+
+    # add all nodes to this mesh group
+    for activeMeshIndex in range(len(meshDataList)):
+        mesh_name = meshDataList[activeMeshIndex]
+        mesh_path = mesh_name.get_path()
+        sceneManifest.mesh_group_select_node(mesh_group, mesh_path)
+
+def create_shape_configuration(nodeName):
+    import scene_api.physics_data
+    
+    if(nodeName in ['_foot_','_wrist_']):
+        shapeConfiguration = scene_api.physics_data.BoxShapeConfiguration()
+        shapeConfiguration.scale = [1.1, 1.1, 1.1]
+        shapeConfiguration.dimensions = [2.1, 3.1, 4.1]
+        return shapeConfiguration
+    else:
+        shapeConfiguration = scene_api.physics_data.CapsuleShapeConfiguration()
+        shapeConfiguration.scale = [1.0, 1.0, 1.0]
+        shapeConfiguration.height = 1.0
+        shapeConfiguration.radius = 1.0
+        return shapeConfiguration
+    
+def create_collider_configuration(nodeName):
+    import scene_api.physics_data
+    
+    colliderConfiguration = scene_api.physics_data.ColliderConfiguration()
+    colliderConfiguration.Position = [0.1, 0.1, 0.2]
+    colliderConfiguration.Rotation = [45.0, 35.0, 25.0]    
+    return colliderConfiguration
+
+def generate_physics_nodes(actorPhysicsSetupRule, nodeNameList):
+    import scene_api.physics_data
+    
+    hitDetectionConfig = scene_api.physics_data.CharacterColliderConfiguration()    
+    simulatedObjectColliderConfig = scene_api.physics_data.CharacterColliderConfiguration()
+    clothConfig = scene_api.physics_data.CharacterColliderConfiguration()
+    ragdollConfig = scene_api.physics_data.RagdollConfiguration()    
+    
+    for nodeName in nodeNameList:
+        shapeConfiguration = create_shape_configuration(nodeName)
+        colliderConfiguration = create_collider_configuration(nodeName)
+        hitDetectionConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
+        simulatedObjectColliderConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
+        clothConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
+        #
+        ragdollNode = scene_api.physics_data.RagdollNodeConfiguration()
+        ragdollNode.JointConfig.Name = nodeName
+        ragdollConfig.add_ragdoll_node_configuration(ragdollNode)
+        ragdollConfig.colliders.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
+    
+    actorPhysicsSetupRule.set_simulated_object_collider_config(simulatedObjectColliderConfig)
+    actorPhysicsSetupRule.set_hit_detection_config(hitDetectionConfig)
+    actorPhysicsSetupRule.set_cloth_config(clothConfig)
+    actorPhysicsSetupRule.set_ragdoll_config(ragdollConfig)
+
+def generate_actor_group(scene, sceneManifest, meshDataList, paths):
+    import scene_api.scene_data
+    import scene_api.physics_data
+    import scene_api.actor_group
+    
+    # fetch bone data
+    validNames = ['_neck_','_pelvis_','_leg_','_knee_','_spine_','_arm_','_clavicle_','_head_','_elbow_','_wrist_']
+    graph = scene_api.scene_data.SceneGraph(scene.graph)
+    nodeList, allNodePaths = get_node_names(graph, 'BoneData', validList = validNames)
+
+    nodeNameList = []
+    for activeMeshIndex, nodeName in enumerate(nodeList):
+        nodeNameList.append(nodeName.get_name())
+
+    # add comment    
+    commentRule = scene_api.actor_group.CommentRule()
+    commentRule.text = str(nodeNameList)
+    
+    # ActorPhysicsSetupRule
+    actorPhysicsSetupRule = scene_api.actor_group.ActorPhysicsSetupRule()
+    generate_physics_nodes(actorPhysicsSetupRule, nodeNameList)
+    
+    # add scale of the Actor rule
+    actorScaleRule = scene_api.actor_group.ActorScaleRule()
+    actorScaleRule.scaleFactor = 2.0
+    
+    # add coordinate system rule
+    coordinateSystemRule = scene_api.actor_group.CoordinateSystemRule()
+    coordinateSystemRule.useAdvancedData = False
+    
+    # add morph target rule
+    morphTargetRule = scene_api.actor_group.MorphTargetRule()
+    morphTargetRule.targets.select_targets([nodeNameList[0]], nodeNameList)
+    
+    # add skeleton optimization rule
+    skeletonOptimizationRule = scene_api.actor_group.SkeletonOptimizationRule()
+    skeletonOptimizationRule.autoSkeletonLOD = True
+    skeletonOptimizationRule.criticalBonesList.select_targets([nodeNameList[0:2]], nodeNameList)
+    
+    # add LOD rule
+    lodRule = scene_api.actor_group.LodRule()
+    lodRule0 = lodRule.add_lod_level(0)
+    lodRule0.select_targets([nodeNameList[1:4]], nodeNameList)
+
+    actorGroup = scene_api.actor_group.ActorGroup()
+    actorGroup.name = os.path.basename(scene.sourceFilename)
+    actorGroup.add_rule(actorScaleRule)
+    actorGroup.add_rule(coordinateSystemRule)
+    actorGroup.add_rule(skeletonOptimizationRule)
+    actorGroup.add_rule(morphTargetRule)
+    actorGroup.add_rule(lodRule)
+    actorGroup.add_rule(actorPhysicsSetupRule)
+    actorGroup.add_rule(commentRule)
+    sceneManifest.manifest['values'].append(actorGroup.to_dict())
+    
+def update_manifest(scene):
+    import json, uuid, os
+    import azlmbr.scene.graph
+    import scene_api.scene_data
+
+    graph = scene_api.scene_data.SceneGraph(scene.graph)
+    mesh_name_list, all_node_paths = get_node_names(graph, 'MeshData')
+    scene_manifest = scene_api.scene_data.SceneManifest()   
+    generate_actor_group(scene, scene_manifest, mesh_name_list, all_node_paths)
+    generate_mesh_group(scene, scene_manifest, mesh_name_list, all_node_paths)
+
+    # Convert the manifest to a JSON string and return it
+    return scene_manifest.export()
+
+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.disconnect()
+    sceneJobHandler = None
+
+# try to create SceneAPI handler for processing
+try:
+    import azlmbr.scene
+
+    sceneJobHandler = azlmbr.scene.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.connect()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+except:
+    sceneJobHandler = None

+ 3 - 0
Code/Tools/SceneAPI/SceneData/GraphData/BoneData.cpp

@@ -46,6 +46,9 @@ namespace AZ
                 BehaviorContext* behaviorContext = azrtti_cast<BehaviorContext*>(context);
                 if (behaviorContext)
                 {
+                    behaviorContext->Class<SceneAPI::DataTypes::IBoneData>()
+                        ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                        ->Attribute(AZ::Script::Attributes::Module, "scene");
                     behaviorContext->Class<AZ::SceneData::GraphData::BoneData>()
                         ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
                         ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)

+ 519 - 0
Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py

@@ -0,0 +1,519 @@
+"""
+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 json
+import uuid
+import os, sys
+
+sys.path.append(os.path.dirname(__file__))
+import physics_data
+
+class ActorGroup():
+    """
+    Configure actor data exporting.
+
+    Attributes
+    ----------
+    name: 
+        Name for the group. This name will also be used as the name for the generated file.
+
+    selectedRootBone: 
+        The root bone of the animation that will be exported.
+
+    rules: `list` of actor rules (derived from BaseRule)
+        modifiers to fine-tune the export process.
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+
+    add_rule(rule)
+        Adds a rule into the internal rules container
+        Returns True if the rule was added to the internal rules container
+
+    create_rule(rule)
+        Helper method to add and return the rule
+
+    remove_rule(type)
+        Removes the rule from the internal rules container
+
+    to_dict()
+        Converts the contents to as a Python dictionary
+
+    to_json()
+        Converts the contents to a JSON string
+
+    """
+    def __init__(self):
+        self.typename = 'ActorGroup'
+        self.name = ''
+        self.selectedRootBone = ''
+        self.id = uuid.uuid4()
+        self.rules = set()
+
+    def add_rule(self, rule) -> bool:
+        if (rule not in self.rules):
+            self.rules.add(rule)
+            return True
+        return False
+
+    def create_rule(self, rule) -> any:
+        if (self.add_rule(rule)):
+            return rule
+        return None
+
+    def remove_rule(self, type) -> None:
+        self.rules.discard(rule)
+
+    def to_dict(self) -> dict:
+        out = {}
+        out['$type'] = self.typename
+        out['name'] = self.name
+        out['selectedRootBone'] = self.selectedRootBone
+        out['id'] = f"{{{str(self.id)}}}"
+        # convert the rules
+        ruleList = []
+        for rule in self.rules:
+            jsonStr = json.dumps(rule, cls=RuleEncoder)
+            jsonDict = json.loads(jsonStr)
+            ruleList.append(jsonDict)
+        out['rules'] = ruleList
+        return out
+
+    def to_json(self) -> any:
+        jsonDOM = self.to_dict()
+        return json.dumps(jsonDOM, cls=RuleEncoder, indent=1)
+
+class RuleEncoder(json.JSONEncoder):
+    """
+    A helper class to encode the Python classes with to a Python dictionary
+
+    Methods
+    -------
+
+    encode(obj)
+        Converts contents to a Python dictionary for the JSONEncoder
+
+    """
+    def encode(self, obj):
+        chunk = obj
+        if isinstance(obj, BaseRule):
+            chunk = obj.to_dict()
+        elif isinstance(obj, dict):
+            chunk = obj
+        elif hasattr(obj, 'to_dict'):
+            chunk = obj.to_dict()
+        else:
+            chunk = obj.__dict__
+        
+        return super().encode(chunk)
+
+class BaseRule():
+    """
+    Base class of the actor rules to help encode the type name of abstract rules
+
+    Parameters
+    ----------
+    typename : str
+        A typename the $type will be in the JSON chunk object
+
+    Attributes
+    ----------
+    typename: str
+        The type name of the abstract classes to be serialized
+
+    id: UUID
+        a unique ID for the rule
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+        Adds the '$type' member
+        Adds a random 'id' member
+    """
+    def __init__(self, typename):
+        self.typename = typename
+        self.id = uuid.uuid4()
+        
+    def __eq__(self, other):
+        return self.id.__eq__(other.id)
+
+    def __ne__(self, other):
+        return self.__eq__(other) is False
+
+    def __hash__(self):
+        return self.id.__hash__()
+    
+    def to_dict(self):
+        data = self.__dict__
+        data['id'] = f"{{{str(self.id)}}}"
+        # rename 'typename' to '$type'
+        data['$type'] = self.typename
+        data.pop('typename')
+        return data
+
+class SceneNodeSelectionList(BaseRule):
+    """
+    Contains a list of node names to include (selectedNodes) and to exclude (unselectedNodes)
+
+    Attributes
+    ----------
+    selectedNodes: `list` of str
+        The node names to include for this group rule
+        
+    unselectedNodes: `list` of str
+        The node names to exclude for this group rule
+
+    Methods
+    -------
+    convert_selection(self, container, key):
+        this adds its contents to an existing dictionary container at a key position
+
+    select_targets(self, selectedList, allNodesList:list)
+        helper function to include a small list of node names from list of all the node names
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('SceneNodeSelectionList')
+        self.selectedNodes = []
+        self.unselectedNodes = []
+
+    def convert_selection(self, container, key):
+        container[key] = self.to_dict()
+        
+    def select_targets(self, selectedList, allNodesList:list):
+        self.selectedNodes = selectedList
+        self.unselectedNodes = allNodesList.copy()
+        for node in selectedList:
+            if node in self.unselectedNodes:
+                self.unselectedNodes.remove(node)
+
+class LodNodeSelectionList(SceneNodeSelectionList):
+    """
+    Level of Detail node selection list
+    
+    The selected nodes should be joints with BoneData
+    derived from SceneNodeSelectionList
+    see also LodRule
+
+    Attributes
+    ----------
+    lodLevel: int
+        the level of detail to target where 0 is nearest level of detail
+        up to 5 being the farthest level of detail range for 6 levels maximum
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__()
+        self.typename = 'LodNodeSelectionList'
+        self.lodLevel = 0
+
+    def to_dict(self):
+        data = super().to_dict()
+        data['lodLevel'] = self.lodLevel
+        return data
+
+class PhysicsAnimationConfiguration():
+    """
+    Configuration for animated physics structures which are more detailed than the character controller.
+    For example, ragdoll or hit detection configurations.
+    See also 'class Physics::AnimationConfiguration'
+
+    Attributes
+    ----------
+    hitDetectionConfig: CharacterColliderConfiguration
+        for hit detection
+
+    ragdollConfig: RagdollConfiguration
+        to set up physics properties
+
+    clothConfig: CharacterColliderConfiguration
+        for cloth physics
+
+    simulatedObjectColliderConfig: CharacterColliderConfiguration
+        for simulation physics
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.hitDetectionConfig = physics_data.CharacterColliderConfiguration()
+        self.ragdollConfig = physics_data.RagdollConfiguration()
+        self.clothConfig = physics_data.CharacterColliderConfiguration()
+        self.simulatedObjectColliderConfig = physics_data.CharacterColliderConfiguration()
+        
+    def to_dict(self):
+        data = {}
+        data["hitDetectionConfig"] = self.hitDetectionConfig.to_dict()
+        data["ragdollConfig"] = self.ragdollConfig.to_dict()
+        data["clothConfig"] = self.clothConfig.to_dict()
+        data["simulatedObjectColliderConfig"] = self.simulatedObjectColliderConfig.to_dict()
+        return data
+
+class EMotionFXPhysicsSetup():
+    """
+    Physics setup properties
+    See also 'class EMotionFX::PhysicsSetup'
+
+    Attributes
+    ----------
+    config: PhysicsAnimationConfiguration
+        Configuration to setup physics properties
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.typename = 'PhysicsSetup'
+        self.config = PhysicsAnimationConfiguration()
+
+    def to_dict(self):
+        return { 
+            "config" : self.config.to_dict() 
+        }
+
+class ActorPhysicsSetupRule(BaseRule):
+    """
+    Physics setup properties
+
+    Attributes
+    ----------
+    data: EMotionFXPhysicsSetup
+        Data to setup physics properties
+
+    Methods
+    -------
+
+    set_hit_detection_config(self, hitDetectionConfig)
+        Simple helper function to assign the hit detection configuration
+
+    set_ragdoll_config(self, ragdollConfig)
+        Simple helper function to assign the ragdoll configuration
+
+    set_cloth_config(self, clothConfig)
+        Simple helper function to assign the cloth configuration
+
+    set_simulated_object_collider_config(self, simulatedObjectColliderConfig)
+        Simple helper function to assign the assign simulated object collider configuration
+
+    to_dict()
+        Converts contents to a Python dictionary
+
+    """
+    def __init__(self):
+        super().__init__('ActorPhysicsSetupRule')
+        self.data = EMotionFXPhysicsSetup()
+        
+    def set_hit_detection_config(self, hitDetectionConfig) -> None:
+        self.data.config.hitDetectionConfig = hitDetectionConfig
+
+    def set_ragdoll_config(self, ragdollConfig) -> None:
+        self.data.config.ragdollConfig = ragdollConfig
+
+    def set_cloth_config(self, clothConfig) -> None:
+        self.data.config.clothConfig = clothConfig
+
+    def set_simulated_object_collider_config(self, simulatedObjectColliderConfig) -> None:
+        self.data.config.simulatedObjectColliderConfig = simulatedObjectColliderConfig
+
+    def to_dict(self):
+        data = super().to_dict()
+        data["data"] = self.data.to_dict()
+        return data       
+
+class ActorScaleRule(BaseRule):
+    """
+    Scale the actor
+
+    Attributes
+    ----------
+    scaleFactor: float
+        Set the multiplier to scale geometry.
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+
+    """
+    def __init__(self):
+        super().__init__('ActorScaleRule')
+        self.scaleFactor = 1.0           
+
+class CoordinateSystemRule(BaseRule):
+    """
+    Modify the target coordinate system, applying a transformation to all data (transforms and vertex data if it exists).
+
+    Attributes
+    ----------
+    targetCoordinateSystem: int
+        Change the direction the actor/motion will face by applying a post transformation to the data.
+
+    useAdvancedData: bool
+        If True, use advanced settings
+
+    originNodeName: str
+        Select a Node from the scene as the origin for this export.
+
+    rotation: [float, float, float, float]
+        Sets the orientation offset of the processed mesh in degrees. Rotates (yaw, pitch, roll) the group after translation.
+
+    translation: [float, float, float] 
+        Moves the group along the given vector3.
+
+    scale: float
+        Sets the scale offset of the processed mesh.
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('CoordinateSystemRule')
+        self.targetCoordinateSystem = 0
+        self.useAdvancedData = False
+        self.originNodeName = ''
+        self.rotation = [0.0, 0.0, 0.0, 1.0]
+        self.translation = [0.0, 0.0, 0.0]
+        self.scale = 1.0
+
+class SkeletonOptimizationRule(BaseRule):
+    """
+    Advanced skeleton optimization rule.
+
+    Attributes
+    ----------
+    autoSkeletonLOD: bool
+        Client side skeleton LOD based on skinning information and critical bones list.
+
+    serverSkeletonOptimization: bool
+        Server side skeleton optimization based on hit detections and critical bones list.
+
+    criticalBonesList: `list` of SceneNodeSelectionList
+        Bones in this list will not be optimized out.
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('SkeletonOptimizationRule')
+        self.autoSkeletonLOD = True
+        self.serverSkeletonOptimization = False
+        self.criticalBonesList = SceneNodeSelectionList()
+
+    def to_dict(self):
+        data = super().to_dict()
+        self.criticalBonesList.convert_selection(data, 'criticalBonesList')
+        return data
+
+class LodRule(BaseRule):
+    """
+    Set up the level of detail for the meshes in this group.
+
+    The engine supports 6 total lods.
+    1 for the base model then 5 more lods.  
+    The rule only captures lods past level 0 so this is set to 5. 
+
+    Attributes
+    ----------
+    nodeSelectionList: `list` of LodNodeSelectionList
+        Select the meshes to assign to each level of detail.
+
+    Methods
+    -------
+
+    add_lod_level(lodLevel, selectedNodes=None, unselectedNodes=None)
+        A helper function to add selected nodes (list of node names) and unselected nodes (list of node names)
+        This creates a LodNodeSelectionList and adds it to the node selection list
+        returns the LodNodeSelectionList that was created
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('{3CB103B3-CEAF-49D7-A9DC-5A31E2DF15E4} LodRule')
+        self.nodeSelectionList = [] # list of LodNodeSelectionList
+        
+    def add_lod_level(self, lodLevel, selectedNodes=None, unselectedNodes=None) -> LodNodeSelectionList:
+        lodNodeSelection = LodNodeSelectionList()
+        lodNodeSelection.selectedNodes = selectedNodes
+        lodNodeSelection.unselectedNodes = unselectedNodes
+        lodNodeSelection.lodLevel = lodLevel
+        self.nodeSelectionList.append(lodNodeSelection)
+        return lodNodeSelection
+        
+    def to_dict(self):
+        data = super().to_dict()
+        selectionListList = data.pop('nodeSelectionList')
+        data['nodeSelectionList'] = []
+        for nodeList in selectionListList:
+            data['nodeSelectionList'].append(nodeList.to_dict())        
+        return data
+
+class MorphTargetRule(BaseRule):
+    """
+    Select morph targets for actor.
+
+    Attributes
+    ----------
+    targets: `list` of SceneNodeSelectionList
+        Select 1 or more meshes to include in the actor as morph targets.
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('MorphTargetRule')
+        self.targets = SceneNodeSelectionList()
+
+    def to_dict(self):
+        data = super().to_dict()
+        self.targets.convert_selection(data, 'targets')
+        return data
+
+class CommentRule(BaseRule):
+    """
+    Add an optional comment to the asset's properties.
+
+    Attributes
+    ----------
+    text: str
+        Text for the comment.
+
+    Methods
+    -------
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('CommentRule')
+        self.text = ''

+ 580 - 0
Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py

@@ -0,0 +1,580 @@
+"""
+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
+"""
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Shape.h
+
+class ColliderConfiguration():
+    """
+    Configuration for a collider
+
+    Attributes
+    ----------
+    Trigger: bool
+        Should this shape act as a trigger shape.
+
+    Simulated: bool
+        Should this shape partake in collision in the physical simulation.
+
+    InSceneQueries: bool
+        Should this shape partake in scene queries (ray casts, overlap tests, sweeps).
+
+    Exclusive: bool
+        Can this collider be shared between multiple bodies?
+
+    Position: [float, float, float] Vector3
+        Shape offset relative to the connected rigid body.
+
+    Rotation: [float, float, float, float] Quaternion
+        Shape rotation relative to the connected rigid body.
+
+    ColliderTag: str
+        Identification tag for the collider.
+
+    RestOffset: float
+        Bodies will come to rest separated by the sum of their rest offsets.
+
+    ContactOffset: float
+        Bodies will start to generate contacts when closer than the sum of their contact offsets.
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.Trigger = True
+        self.Simulated = True
+        self.InSceneQueries = True
+        self.Exclusive = True
+        self.Position = [0.0, 0.0, 0.0]
+        self.Rotation = [0.0, 0.0, 0.0, 1.0]
+        self.ColliderTag = ''
+        self.RestOffset = 0.0
+        self.ContactOffset = 0.02
+
+    def to_dict(self):
+        return self.__dict__
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Character.h
+
+class CharacterColliderNodeConfiguration():
+    """
+    Shapes to define the animation's to model of physics
+
+    Attributes
+    ----------
+    name : str
+        debug name of the node
+
+    shapes : `list` of `tuple` of (ColliderConfiguration, ShapeConfiguration) 
+        a list of pairs of collider and shape configuration
+
+    Methods
+    -------
+    add_collider_shape_pair(colliderConfiguration, shapeConfiguration)
+        Helper function to add a collider and shape configuration at the same time
+
+    to_dict()
+        Converts contents to a Python dictionary
+
+    """
+    def __init__(self):
+        self.name = ''
+        self.shapes = [] # List of Tuple of (ColliderConfiguration, ShapeConfiguration) 
+        
+    def add_collider_shape_pair(self, colliderConfiguration, shapeConfiguration) -> None:
+        pair = (colliderConfiguration, shapeConfiguration)
+        self.shapes.append(pair)
+        
+    def to_dict(self):
+        data = {}
+        shapeList = []
+        for index, shape in enumerate(self.shapes):
+            tupleValue = (shape[0].to_dict(),  # ColliderConfiguration
+                          shape[1].to_dict())  # ShapeConfiguration
+            shapeList.append(tupleValue)
+        data['name'] = self.name
+        data['shapes'] = shapeList
+        return data
+
+class CharacterColliderConfiguration():
+    """
+    Information required to create the basic physics representation of a character.
+
+    Attributes
+    ----------
+    nodes : `list` of CharacterColliderNodeConfiguration
+        a list of CharacterColliderNodeConfiguration nodes
+
+    Methods
+    -------
+    add_character_collider_node_configuration(colliderConfiguration, shapeConfiguration)
+        Helper function to add a character collider node configuration into the nodes
+
+    add_character_collider_node_configuration_node(name, colliderConfiguration, shapeConfiguration)
+        Helper function to add a character collider node configuration into the nodes 
+
+        **Returns**: CharacterColliderNodeConfiguration
+
+    to_dict()
+        Converts contents to a Python dictionary
+
+    """
+    def __init__(self):
+        self.nodes = [] # list of CharacterColliderNodeConfiguration
+        
+    def add_character_collider_node_configuration(self, characterColliderNodeConfiguration) -> None:
+        self.nodes.append(characterColliderNodeConfiguration)
+        
+    def add_character_collider_node_configuration_node(self, name, colliderConfiguration, shapeConfiguration) -> CharacterColliderNodeConfiguration:
+        characterColliderNodeConfiguration = CharacterColliderNodeConfiguration()
+        self.add_character_collider_node_configuration(characterColliderNodeConfiguration)
+        characterColliderNodeConfiguration.name = name
+        characterColliderNodeConfiguration.add_collider_shape_pair(colliderConfiguration, shapeConfiguration)
+        return characterColliderNodeConfiguration
+
+    def to_dict(self):
+        data = {}
+        nodeList = []
+        for node in self.nodes:
+            nodeList.append(node.to_dict())
+        data['nodes'] = nodeList
+        return data
+
+# see Code\Framework\AzFramework\AzFramework\Physics\ShapeConfiguration.h for underlying data structures
+
+class ShapeConfiguration():
+    """
+    Base class for all the shape collider configurations
+
+    Attributes
+    ----------
+    scale : [float, float, float]
+        a 3-element list to describe the scale along the X, Y, and Z axises such as [1.0, 1.0, 1.0]
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self, shapeType):
+        self._shapeType = shapeType
+        self.scale = [1.0, 1.0, 1.0]
+
+    def to_dict(self):
+        return {
+            "$type": self._shapeType,
+            "Scale": self.scale
+        }
+    
+class SphereShapeConfiguration(ShapeConfiguration):
+    """
+    The configuration for a Sphere collider
+
+    Attributes
+    ----------
+    radius: float
+        a scalar value to define the radius of the sphere
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('SphereShapeConfiguration')
+        self.radius = 0.5
+
+    def to_dict(self):
+        data = super().to_dict()
+        data['Radius'] = self.radius
+        return data    
+
+class BoxShapeConfiguration(ShapeConfiguration):
+    """
+    The configuration for a Box collider
+
+    Attributes
+    ----------
+    dimensions: [float, float, float]
+        The width, height, and depth dimensions of the Box collider
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('BoxShapeConfiguration')
+        self.dimensions = [1.0, 1.0, 1.0]
+
+    def to_dict(self):
+        data = super().to_dict()
+        data['Configuration'] = self.dimensions
+        return data
+    
+class CapsuleShapeConfiguration(ShapeConfiguration):
+    """
+    The configuration for a Capsule collider
+
+    Attributes
+    ----------
+    height: float
+        The height of the Capsule
+
+    radius: float
+        The radius of the Capsule
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('CapsuleShapeConfiguration')
+        self.height = 1.00
+        self.radius = 0.25
+    
+    def to_dict(self):
+        data = super().to_dict()
+        data['Height'] = self.height
+        data['Radius'] = self.radius
+        return data    
+    
+class PhysicsAssetShapeConfiguration(ShapeConfiguration):
+    """
+    The configuration for a Asset collider using a mesh asset for collision
+
+    Attributes
+    ----------
+    asset: { "assetHint": assetReference }
+        the name of the asset to load for collision information
+
+    assetScale: [float, float, float]
+        The scale of the asset shape such as [1.0, 1.0, 1.0]
+
+    useMaterialsFromAsset: bool
+        Auto-set physics materials using asset's physics material names
+
+    subdivisionLevel: int
+        The level of subdivision if a primitive shape is replaced with a convex mesh due to scaling.
+
+    Methods
+    -------
+    set_asset_reference(self, assetReference: str)
+        Helper function to set the asset reference to the collision mesh such as 'my/folder/my_mesh.azmodel'
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__('PhysicsAssetShapeConfiguration')
+        self.asset = {}
+        self.assetScale = [1.0, 1.0, 1.0]
+        self.useMaterialsFromAsset = True
+        self.subdivisionLevel = 4
+        
+    def set_asset_reference(self, assetReference: str) -> None:
+        self.asset = { "assetHint": assetReference }
+
+    def to_dict(self):
+        data = super().to_dict()
+        data['PhysicsAsset'] = self.asset
+        data['AssetScale'] = self.assetScale
+        data['UseMaterialsFromAsset'] = self.useMaterialsFromAsset
+        data['SubdivisionLevel'] = self.subdivisionLevel
+        return data    
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Configuration\JointConfiguration.h
+
+class JointConfiguration():
+    """
+    The joint configuration
+
+    see also: class AzPhysics::JointConfiguration
+
+    Attributes
+    ----------
+    Name: str
+        For debugging/tracking purposes only.
+
+    ParentLocalRotation: [float, float, float, float]
+        Parent joint frame relative to parent body.
+
+    ParentLocalPosition: [float, float, float]
+        Joint position relative to parent body.
+
+    ChildLocalRotation: [float, float, float, float]
+        Child joint frame relative to child body.
+
+    ChildLocalPosition: [float, float, float]
+        Joint position relative to child body.
+
+    StartSimulationEnabled: bool 
+        When active, the joint will be enabled when the simulation begins.
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.Name = ''
+        self.ParentLocalRotation = [0.0, 0.0, 0.0, 1.0]
+        self.ParentLocalPosition = [0.0, 0.0, 0.0]
+        self.ChildLocalRotation = [0.0, 0.0, 0.0, 1.0]
+        self.ChildLocalPosition = [0.0, 0.0, 0.0]
+        self.StartSimulationEnabled = True
+
+    def to_dict(self):
+        return self.__dict__
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Configuration\SimulatedBodyConfiguration.h
+
+class SimulatedBodyConfiguration():
+    """
+    Base Class of all Physics Bodies that will be simulated.
+
+    see also: class AzPhysics::SimulatedBodyConfiguration
+
+    Attributes
+    ----------
+    name: str
+        For debugging/tracking purposes only.
+
+    position: [float, float, float]
+        starting position offset
+
+    orientation: [float, float, float, float]
+        starting rotation (Quaternion)
+
+    startSimulationEnabled: bool
+        to start when simulation engine starts
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.name = ''
+        self.position = [0.0, 0.0, 0.0]
+        self.orientation = [0.0, 0.0, 0.0, 1.0]
+        self.startSimulationEnabled = True
+
+    def to_dict(self):
+        return {
+            "name" : self.name,
+            "position" : self.position,
+            "orientation" : self.orientation,
+            "startSimulationEnabled" : self.startSimulationEnabled
+        }
+    
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Configuration\RigidBodyConfiguration.h
+
+class RigidBodyConfiguration(SimulatedBodyConfiguration):
+    """
+    PhysX Rigid Body Configuration
+
+    see also: class AzPhysics::RigidBodyConfiguration
+
+    Attributes
+    ----------
+    initialLinearVelocity: [float, float, float]
+        Linear velocity applied when the rigid body is activated.
+
+    initialAngularVelocity: [float, float, float]
+        Angular velocity applied when the rigid body is activated (limited by maximum angular velocity)
+
+    centerOfMassOffset: [float, float, float]
+        Local space offset for the center of mass (COM).
+
+    mass: float
+        The mass of the rigid body in kilograms. 
+        A value of 0 is treated as infinite.
+        The trajectory of infinite mass bodies cannot be affected by any collisions or forces other than gravity.
+
+    linearDamping: float
+        The rate of decay over time for linear velocity even if no forces are acting on the rigid body.
+
+    angularDamping: float
+        The rate of decay over time for angular velocity even if no forces are acting on the rigid body.
+
+    sleepMinEnergy: float
+        The rigid body can go to sleep (settle) when kinetic energy per unit mass is persistently below this value.
+
+    maxAngularVelocity: float
+        Clamp angular velocities to this maximum value.
+
+    startAsleep: bool
+        When active, the rigid body will be asleep when spawned, and wake when the body is disturbed.
+
+    interpolateMotion: bool
+        When active, simulation results are interpolated resulting in smoother motion.
+
+    gravityEnabled: bool
+        When active, global gravity affects this rigid body.
+
+    kinematic: bool
+        When active, the rigid body is not affected by gravity or other forces and is moved by script.
+
+    ccdEnabled: bool
+        When active, the rigid body has continuous collision detection (CCD). 
+        Use this to ensure accurate collision detection, particularly for fast moving rigid bodies. 
+        CCD must be activated in the global PhysX preferences.
+
+    ccdMinAdvanceCoefficient: float
+        Coefficient affecting how granularly time is subdivided in CCD.
+
+    ccdFrictionEnabled: bool
+        Whether friction is applied when resolving CCD collisions.
+
+    computeCenterOfMass: bool
+        Compute the center of mass (COM) for this rigid body.
+
+    computeInertiaTensor: bool
+        When active, inertia is computed based on the mass and shape of the rigid body.
+
+    computeMass: bool
+        When active, the mass of the rigid body is computed based on the volume and density values of its colliders.
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__()
+
+        # Basic initial settings.
+        self.initialLinearVelocity = [0.0, 0.0, 0.0]
+        self.initialAngularVelocity = [0.0, 0.0, 0.0]
+        self.centerOfMassOffset = [0.0, 0.0, 0.0]
+
+        # Simulation parameters.
+        self.mass = 1.0
+        self.linearDamping = 0.05
+        self.angularDamping = 0.15
+        self.sleepMinEnergy = 0.005
+        self.maxAngularVelocity = 100.0
+        self.startAsleep = False
+        self.interpolateMotion = False
+        self.gravityEnabled = True
+        self.kinematic = False
+        self.ccdEnabled = False
+        self.ccdMinAdvanceCoefficient = 0.15
+        self.ccdFrictionEnabled = False
+        self.computeCenterOfMass = True
+        self.computeInertiaTensor = True
+        self.computeMass = True
+
+        # Flags to restrict motion along specific world-space axes.
+        self.lockLinearX = False
+        self.lockLinearY = False
+        self.lockLinearZ = False
+
+        # Flags to restrict rotation around specific world-space axes.
+        self.lockAngularX = False
+        self.lockAngularY = False
+        self.lockAngularZ = False
+
+        # If set, non-simulated shapes will also be included in the mass properties calculation.
+        self.includeAllShapesInMassCalculation = False
+
+    def to_dict(self):
+        data = super().to_dict()
+        data["Initial linear velocity"] = self.initialLinearVelocity
+        data["Initial angular velocity"] = self.initialAngularVelocity
+        data["Linear damping"] = self.linearDamping
+        data["Angular damping"] = self.angularDamping
+        data["Sleep threshold"] = self.sleepMinEnergy
+        data["Start Asleep"] = self.startAsleep
+        data["Interpolate Motion"] = self.interpolateMotion
+        data["Gravity Enabled"] = self.gravityEnabled
+        data["Kinematic"] = self.kinematic
+        data["CCD Enabled"] = self.ccdEnabled
+        data["Compute Mass"] = self.computeMass
+        data["Lock Linear X"] = self.lockLinearX
+        data["Lock Linear Y"] = self.lockLinearY
+        data["Lock Linear Z"] = self.lockLinearZ
+        data["Lock Angular X"] = self.lockAngularX
+        data["Lock Angular Y"] = self.lockAngularY
+        data["Lock Angular Z"] = self.lockAngularZ
+        data["Mass"] = self.mass
+        data["Compute COM"] = self.computeCenterOfMass
+        data["Centre of mass offset"] = self.centerOfMassOffset
+        data["Compute inertia"] = self.computeInertiaTensor
+        data["Maximum Angular Velocity"] = self.maxAngularVelocity
+        data["Include All Shapes In Mass"] = self.includeAllShapesInMassCalculation
+        data["CCD Min Advance"] = self.ccdMinAdvanceCoefficient
+        data["CCD Friction"] = self.ccdFrictionEnabled        
+        return data
+
+# for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Ragdoll.h
+
+class RagdollNodeConfiguration(RigidBodyConfiguration):
+    """
+    Ragdoll node Configuration
+
+    see also: class Physics::RagdollConfiguration
+
+    Attributes
+    ----------
+    JointConfig: JointConfiguration
+        Ragdoll joint node configuration
+
+    Methods
+    -------
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        super().__init__()
+        self.JointConfig = JointConfiguration()
+
+    def to_dict(self):
+        data = super().to_dict()
+        data['JointConfig'] = self.JointConfig.to_dict()
+        return data
+
+class RagdollConfiguration():
+    """
+    A configuration of join nodes and a character collider configuration for a ragdoll
+
+    see also: class Physics::RagdollConfiguration
+
+    Attributes
+    ----------
+    nodes: `list` of  RagdollNodeConfiguration
+        A list of RagdollNodeConfiguration entries
+
+    colliders: CharacterColliderConfiguration
+        A CharacterColliderConfiguration
+
+    Methods
+    -------
+    add_ragdoll_node_configuration(ragdollNodeConfiguration)
+        Helper function to add a single ragdoll node configuration (normally for each joint/bone node)
+
+    to_dict()
+        Converts contents to a Python dictionary
+    """
+    def __init__(self):
+        self.nodes = [] # list of RagdollNodeConfiguration
+        self.colliders = CharacterColliderConfiguration()
+        
+    def add_ragdoll_node_configuration(self, ragdollNodeConfiguration) -> None:
+        self.nodes.append(ragdollNodeConfiguration)
+
+    def to_dict(self):
+        data = {}
+        nodeList = []
+        for index, node in enumerate(self.nodes):
+            nodeList.append(node.to_dict())
+        data['nodes'] = nodeList
+        data['colliders'] = self.colliders.to_dict()
+        return data