diff --git a/AutomatedTesting/Assets/TestAnim/Test_Jack_Death_Fall_Back_ZUp.fbx b/AutomatedTesting/Assets/TestAnim/Test_Jack_Death_Fall_Back_ZUp.fbx new file mode 100644 index 0000000000..c9df6bfcc1 --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/Test_Jack_Death_Fall_Back_ZUp.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c1d04d687bdce69965965c290c907b3f9a730a7ea73874b734e1c7ff41426d9 +size 3832768 diff --git a/AutomatedTesting/Assets/TestAnim/scene_export_motion.py b/AutomatedTesting/Assets/TestAnim/scene_export_motion.py new file mode 100644 index 0000000000..0591d5b7b4 --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/scene_export_motion.py @@ -0,0 +1,66 @@ +# +# 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 + +import scene_export_utils +import scene_api.motion_group + +# +# Example for exporting MotionGroup scene rules +# + +def update_manifest(scene): + import azlmbr.scene.graph + import scene_api.scene_data + + # create a SceneManifest + sceneManifest = scene_api.scene_data.SceneManifest() + + # create a MotionGroup + motionGroup = scene_api.motion_group.MotionGroup() + motionGroup.name = os.path.basename(scene.sourceFilename.replace('.', '_')) + + motionAdditiveRule = scene_api.motion_group.MotionAdditiveRule() + motionAdditiveRule.sampleFrame = 2 + motionGroup.add_rule(motionAdditiveRule) + + motionScaleRule = motionGroup.create_rule(scene_api.motion_group.MotionScaleRule()) + motionScaleRule.scaleFactor = 1.1 + motionGroup.add_rule(motionScaleRule) + + # add motion group to scene manifest + sceneManifest.add_motion_group(motionGroup) + + # Convert the manifest to a JSON string and return it + return sceneManifest.export() + +sceneJobHandler = None + +def on_update_manifest(args): + try: + scene = args[0] + return update_manifest(scene) + except RuntimeError as err: + print (f'ERROR - {err}') + scene_export_utils.log_exception_traceback() + except: + scene_export_utils.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/AutomatedTesting/Assets/TestAnim/test_jack_death_fall_back_zup.fbx.assetinfo b/AutomatedTesting/Assets/TestAnim/test_jack_death_fall_back_zup.fbx.assetinfo new file mode 100644 index 0000000000..73b42c1e73 --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/test_jack_death_fall_back_zup.fbx.assetinfo @@ -0,0 +1,8 @@ +{ + "values": [ + { + "$type": "ScriptProcessorRule", + "scriptFilename": "Assets/TestAnim/scene_export_motion.py" + } + ] +} \ No newline at end of file diff --git a/AutomatedTesting/Editor/Scripts/scene_export_utils.py b/AutomatedTesting/Editor/Scripts/scene_export_utils.py new file mode 100644 index 0000000000..577bcc9f3e --- /dev/null +++ b/AutomatedTesting/Editor/Scripts/scene_export_utils.py @@ -0,0 +1,63 @@ +# +# 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 + +# +# Utility methods for processing scenes +# + +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 diff --git a/AutomatedTesting/Editor/Scripts/scene_helpers.py b/AutomatedTesting/Editor/Scripts/scene_helpers.py index cae4488abc..e90e7e706a 100644 --- a/AutomatedTesting/Editor/Scripts/scene_helpers.py +++ b/AutomatedTesting/Editor/Scripts/scene_helpers.py @@ -1,10 +1,10 @@ -""" -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 -""" - +# +# 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 diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/__init__.py b/Gems/PythonAssetBuilder/Editor/Scripts/__init__.py index f5193b300e..7a325ca97e 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/__init__.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/__init__.py @@ -1,6 +1,7 @@ -""" -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 -""" +# +# 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 +# +# diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/bootstrap.py b/Gems/PythonAssetBuilder/Editor/Scripts/bootstrap.py index bbcbcf1807..7a325ca97e 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/bootstrap.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/bootstrap.py @@ -1,7 +1,7 @@ -""" -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 -""" - +# +# 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 +# +# diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/_init_.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/_init_.py index f5193b300e..7a325ca97e 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/_init_.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/_init_.py @@ -1,6 +1,7 @@ -""" -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 -""" +# +# 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 +# +# diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py index dd9f1f8729..6878319fb0 100644 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/actor_group.py @@ -1,16 +1,15 @@ -""" -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 -""" - +# +# 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 +import scene_api.physics_data +from scene_api.common_rules import RuleEncoder, BaseRule, SceneNodeSelectionList, CommentRule, CoordinateSystemRule class ActorGroup(): """ @@ -18,10 +17,10 @@ class ActorGroup(): Attributes ---------- - name: + name: Name for the group. This name will also be used as the name for the generated file. - selectedRootBone: + selectedRootBone: The root bone of the animation that will be exported. rules: `list` of actor rules (derived from BaseRule) @@ -86,121 +85,15 @@ class ActorGroup(): out['rules'] = ruleList return out - def to_json(self) -> any: + def to_json(self, i = 0) -> 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 + return json.dumps(jsonDOM, cls=RuleEncoder, indent=i) - 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 @@ -254,11 +147,11 @@ class PhysicsAnimationConfiguration(): 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() - + self.hitDetectionConfig = scene_api.physics_data.CharacterColliderConfiguration() + self.ragdollConfig = scene_api.physics_data.RagdollConfiguration() + self.clothConfig = scene_api.physics_data.CharacterColliderConfiguration() + self.simulatedObjectColliderConfig = scene_api.physics_data.CharacterColliderConfiguration() + def to_dict(self): data = {} data["hitDetectionConfig"] = self.hitDetectionConfig.to_dict() @@ -288,8 +181,8 @@ class EMotionFXPhysicsSetup(): self.config = PhysicsAnimationConfiguration() def to_dict(self): - return { - "config" : self.config.to_dict() + return { + "config" : self.config.to_dict() } class ActorPhysicsSetupRule(BaseRule): @@ -323,7 +216,7 @@ class ActorPhysicsSetupRule(BaseRule): def __init__(self): super().__init__('ActorPhysicsSetupRule') self.data = EMotionFXPhysicsSetup() - + def set_hit_detection_config(self, hitDetectionConfig) -> None: self.data.config.hitDetectionConfig = hitDetectionConfig @@ -339,7 +232,7 @@ class ActorPhysicsSetupRule(BaseRule): def to_dict(self): data = super().to_dict() data["data"] = self.data.to_dict() - return data + return data class ActorScaleRule(BaseRule): """ @@ -358,46 +251,7 @@ class ActorScaleRule(BaseRule): """ 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 + self.scaleFactor = 1.0 class SkeletonOptimizationRule(BaseRule): """ @@ -436,8 +290,8 @@ 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. + 1 for the base model then 5 more lods. + The rule only captures lods past level 0 so this is set to 5. Attributes ---------- @@ -458,7 +312,7 @@ class LodRule(BaseRule): 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 @@ -466,13 +320,13 @@ class LodRule(BaseRule): 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()) + data['nodeSelectionList'].append(nodeList.to_dict()) return data class MorphTargetRule(BaseRule): @@ -499,21 +353,3 @@ class MorphTargetRule(BaseRule): 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/common_rules.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/common_rules.py new file mode 100644 index 0000000000..72cac2b7c6 --- /dev/null +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/common_rules.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, logging + +def log_exception_traceback(): + """ + Outputs an exception stacktrace. + """ + data = traceback.format_exc() + logger = logging.getLogger('python') + logger.error(data) + + +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 + Note: Override this method if a derviced class needs to return a custom dictionary + + """ + 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 = vars(self) + data['id'] = f"{{{str(self.id)}}}" + # rename 'typename' to '$type' + data['$type'] = self.typename + data.pop('typename') + return data + +def convert_rule_to_json(rule:BaseRule, indentValue=0): + """ + Helper function to convert a BaseRule into a JSON string + + Parameters + ---------- + obj : any + The object to convert to a JSON string as long as the obj class has an *to_dict* method + + indentValue : int + The number of spaces to indent between each JSON block/value + """ + return json.dumps(rule.to_dict(), indent=indentValue, cls=RuleEncoder) + +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 = '' + + +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 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. + + """ + 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 TypeId(): + """ + Wraps a UUID that represents a AZ::TypeId from O3DE + + Attributes + ---------- + valud: uuid.Uuid + A unique ID that defaults to AZ::TypeId::CreateNull() + """ + def __init__(self): + self.value = uuid.UUID('{00000000-0000-0000-0000-000000000000}') + + def __str__(self): + return f"{{{str(self.value)}}}" + + +class RuleEncoder(json.JSONEncoder): + """ + A helper class to encode the Python classes with to a Python dictionary + + Methods + ------- + + default(obj) + Converts a single object to a JSON value that can be stored with a key + + encode(obj) + Converts contents to a Python dictionary for the JSONEncoder + + """ + def default(self, obj): + if (isinstance(obj,TypeId)): + return str(obj) + elif hasattr(obj, 'to_json_value'): + return obj.to_json_value() + return super().default(obj) + + 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) diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/motion_group.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/motion_group.py new file mode 100644 index 0000000000..e7d3896e9c --- /dev/null +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/motion_group.py @@ -0,0 +1,214 @@ +# +# 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 +# +# +from enum import Enum +import scene_api.common_rules + +class MotionGroup(scene_api.common_rules.BaseRule): + """ + Configure animation data for exporting. + + Attributes + ---------- + name: str + Name for the group. + This name will also be used as the name for the generated file. + + selectedRootBone: str + The root bone of the animation that will be exported. + + rules: list of BaseRule + Add or remove rules to fine-tune the export process. + List of rules for a motion group including: + MotionScaleRule + CoordinateSystemRule + MotionRangeRule + MotionAdditiveRule + MotionSamplingRule + + """ + def __init__(self): + super().__init__('MotionGroup') + self.name = '' + self.selectedRootBone = '' + 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 = super().to_dict() + out['name'] = self.name + out['selectedRootBone'] = self.selectedRootBone + # convert the rules + ruleList = [] + for rule in self.rules: + ruleList.append(rule.to_dict()) + out['rules'] = ruleList + return out + + def to_json(self) -> str: + jsonDOM = self.to_dict() + return json.dumps(jsonDOM, cls=RuleEncoder) + + +class MotionCompressionSettingsRule(scene_api.common_rules.BaseRule): + """ + A BaseRule that ses the error tolerance settings while compressing the animation + + Attributes + ---------- + maxTranslationError: float + Maximum error allowed in translation. + Min 0.0, Max 0.1 + + maxRotationError: float + Maximum error allowed in rotation. + Min 0.0, Max 0.1 + + maxScaleError: float + Maximum error allowed in scale. + Min 0.0, Max 0.01 + """ + def __init__(self): + super().__init__('MotionCompressionSettingsRule') + self.maxTranslationError = 0.0001 + self.maxRotationError = 0.0001 + self.maxScaleError = 0.0001 + +class MotionScaleRule(scene_api.common_rules.BaseRule): + """ + A BaseRule that scales the spatial extent of motion + + Attributes + ---------- + scaleFactor: float + Scale factor; min 0.0001, max 10000.0 + + """ + def __init__(self): + super().__init__('MotionScaleRule') + self.scaleFactor = 1.0 + + +class MotionRangeRule(scene_api.common_rules.BaseRule): + """ + A BaseRule that defines the range of the motion that will be exported. + + Attributes + ---------- + startFrame: float + The start frame of the animation that will be exported. + + endFrame: float + The end frame of the animation that will be exported. + """ + def __init__(self): + super().__init__('MotionRangeRule') + self.startFrame = 0 + self.endFrame = 0 + + +class MotionAdditiveRule(scene_api.common_rules.BaseRule): + """ + A BaseRule that makes the motion an additive motion. + + Attributes + ---------- + sampleFrame: int + The frame number that the motion will be made relative to. + + """ + def __init__(self): + super().__init__('MotionAdditiveRule') + self.sampleFrame = 0 + + +class SampleRateMethod(Enum): + """ + A collection of settings related to sampling of the motion + + Attributes + ---------- + + FromSourceScene: int, value = 0 + Use the source scene's sample rate + + + Custom: int, value = 1 + Use the use a custom sample rate + """ + FromSourceScene = 0 + Custom = 1 + + def to_json_value(self): + if(self == SampleRateMethod.FromSourceScene): + return 0 + return 1 + + +class MotionSamplingRule(scene_api.common_rules.BaseRule): + """ + A collection of settings related to sampling of the motion + + Attributes + ---------- + motionDataType: scene_api.common_rules.TypeId() + The motion data type to use. This defines how the motion data is stored. + This can have an effect on performance and memory usage. + + sampleRateMethod: SampleRateMethod + Either use the sample rate from the source scene file or use a custom sample rate. + The sample rate is automatically limited to the rate from source scene file (e.g. FBX) + + customSampleRate: float + Overwrite the sample rate of the motion, in frames per second. + Min: 1.0, Max 240.0 + + translationQualityPercentage: float + The percentage of quality for translation. Higher values preserve quality, but increase memory usage. + Min: 1.0, Max 100.0 + + rotationQualityPercentage: float + The percentage of quality for rotation. Higher values preserve quality, but increase memory usage. + Min: 1.0, Max 100.0 + + scaleQualityPercentage: float + The percentage of quality for scale. Higher values preserve quality, but increase memory usage. + Min: 1.0, Max 100.0 + + allowedSizePercentage: float + The percentage of extra memory usage allowed compared to the smallest size. + For example a value of 10 means we are allowed 10 percent more memory worst case, in trade for extra performance. + Allow 15 percent larger size, in trade for performance (in Automatic mode, so when m_motionDataType is a Null typeId). + Min: 0.0, Max 100.0 + + keepDuration: bool + When enabled this keep the duration the same as the Fbx motion duration, even if no joints are animated. + When this option is disabled and the motion doesn't animate any joints then the resulting motion will have a duration of zero seconds. + """ + def __init__(self): + super().__init__('MotionSamplingRule') + self.motionDataType = scene_api.common_rules.TypeId() + self.sampleRateMethod = SampleRateMethod.FromSourceScene + self.customSampleRate = 60.0 + self.translationQualityPercentage = 75.0 + self.rotationQualityPercentage = 75.0 + self.scaleQualityPercentage = 75.0 + self.allowedSizePercentage = 15.0 + self.keepDuration = True diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py index 8dea8d8b3a..4b02e99531 100644 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/physics_data.py @@ -1,9 +1,10 @@ -""" -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 -""" +# +# 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 @@ -69,7 +70,7 @@ class CharacterColliderNodeConfiguration(): name : str debug name of the node - shapes : `list` of `tuple` of (ColliderConfiguration, ShapeConfiguration) + shapes : `list` of `tuple` of (ColliderConfiguration, ShapeConfiguration) a list of pairs of collider and shape configuration Methods @@ -83,12 +84,12 @@ class CharacterColliderNodeConfiguration(): """ def __init__(self): self.name = '' - self.shapes = [] # List of Tuple of (ColliderConfiguration, ShapeConfiguration) - + 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 = [] @@ -115,7 +116,7 @@ class CharacterColliderConfiguration(): 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 + Helper function to add a character collider node configuration into the nodes **Returns**: CharacterColliderNodeConfiguration @@ -125,10 +126,10 @@ class CharacterColliderConfiguration(): """ 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) @@ -169,7 +170,7 @@ class ShapeConfiguration(): "$type": self._shapeType, "Scale": self.scale } - + class SphereShapeConfiguration(ShapeConfiguration): """ The configuration for a Sphere collider @@ -191,7 +192,7 @@ class SphereShapeConfiguration(ShapeConfiguration): def to_dict(self): data = super().to_dict() data['Radius'] = self.radius - return data + return data class BoxShapeConfiguration(ShapeConfiguration): """ @@ -215,7 +216,7 @@ class BoxShapeConfiguration(ShapeConfiguration): data = super().to_dict() data['Configuration'] = self.dimensions return data - + class CapsuleShapeConfiguration(ShapeConfiguration): """ The configuration for a Capsule collider @@ -237,13 +238,13 @@ class CapsuleShapeConfiguration(ShapeConfiguration): 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 - + return data + class PhysicsAssetShapeConfiguration(ShapeConfiguration): """ The configuration for a Asset collider using a mesh asset for collision @@ -276,7 +277,7 @@ class PhysicsAssetShapeConfiguration(ShapeConfiguration): 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 } @@ -286,7 +287,7 @@ class PhysicsAssetShapeConfiguration(ShapeConfiguration): data['AssetScale'] = self.assetScale data['UseMaterialsFromAsset'] = self.useMaterialsFromAsset data['SubdivisionLevel'] = self.subdivisionLevel - return data + return data # for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Configuration\JointConfiguration.h @@ -313,7 +314,7 @@ class JointConfiguration(): ChildLocalPosition: [float, float, float] Joint position relative to child body. - StartSimulationEnabled: bool + StartSimulationEnabled: bool When active, the joint will be enabled when the simulation begins. Methods @@ -372,7 +373,7 @@ class SimulatedBodyConfiguration(): "orientation" : self.orientation, "startSimulationEnabled" : self.startSimulationEnabled } - + # for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Configuration\RigidBodyConfiguration.h @@ -394,7 +395,7 @@ class RigidBodyConfiguration(SimulatedBodyConfiguration): Local space offset for the center of mass (COM). mass: float - The mass of the rigid body in kilograms. + 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. @@ -423,8 +424,8 @@ class RigidBodyConfiguration(SimulatedBodyConfiguration): 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. + 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 @@ -511,7 +512,7 @@ class RigidBodyConfiguration(SimulatedBodyConfiguration): 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 + data["CCD Friction"] = self.ccdFrictionEnabled return data # for underlying data structures, see Code\Framework\AzFramework\AzFramework\Physics\Ragdoll.h @@ -566,7 +567,7 @@ class RagdollConfiguration(): def __init__(self): self.nodes = [] # list of RagdollNodeConfiguration self.colliders = CharacterColliderConfiguration() - + def add_ragdoll_node_configuration(self, ragdollNodeConfiguration) -> None: self.nodes.append(ragdollNodeConfiguration) diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py index 66b836f171..7841f032b9 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py @@ -1,15 +1,15 @@ -""" -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 -""" +# +# 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 typing import json import azlmbr.scene as sceneApi from enum import IntEnum - # Wraps the AZ.SceneAPI.Containers.SceneGraph.NodeIndex internal class class SceneGraphNodeIndex: def __init__(self, scene_graph_node_index) -> None: @@ -199,6 +199,16 @@ class SceneManifest: self.manifest['values'].append(prefab_group) return prefab_group + def add_actor_group(self, group) -> dict: + groupDict = group.to_dict() + self.manifest['values'].append(groupDict) + return groupDict + + def add_motion_group(self, group) -> dict: + groupDict = group.to_dict() + self.manifest['values'].append(groupDict) + return groupDict + def mesh_group_select_node(self, mesh_group: dict, node_name: str) -> None: """Adds a node as a selected node.