diff --git a/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx b/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx new file mode 100644 index 0000000000..f727c26ad2 --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27f87510ae07771dbad3e430e31502b1d1d13dee868f143b7f1de2a7febc8eb9 +size 7046400 diff --git a/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx.assetinfo b/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx.assetinfo new file mode 100644 index 0000000000..2d5502f42f --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/rin_skeleton.fbx.assetinfo @@ -0,0 +1,8 @@ +{ + "values": [ + { + "$type": "ScriptProcessorRule", + "scriptFilename": "Assets/TestAnim/scene_export_actor.py" + } + ] +} \ No newline at end of file diff --git a/AutomatedTesting/Assets/TestAnim/scene_export_actor.py b/AutomatedTesting/Assets/TestAnim/scene_export_actor.py new file mode 100644 index 0000000000..9dddc056a2 --- /dev/null +++ b/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 diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/BoneData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/BoneData.cpp index 8a3807c31a..e1a6c2608f 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/BoneData.cpp +++ b/Code/Tools/SceneAPI/SceneData/GraphData/BoneData.cpp @@ -46,6 +46,9 @@ namespace AZ BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { + behaviorContext->Class() + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) + ->Attribute(AZ::Script::Attributes::Module, "scene"); behaviorContext->Class() ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All) ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py new file mode 100644 index 0000000000..dd9f1f8729 --- /dev/null +++ b/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 = '' diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py new file mode 100644 index 0000000000..8dea8d8b3a --- /dev/null +++ b/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