From 5f41463cd63a0b8d66e074e5c339bd979ff43b70 Mon Sep 17 00:00:00 2001 From: amzn-mike <80125227+amzn-mike@users.noreply.github.com> Date: Fri, 3 Dec 2021 11:23:48 -0600 Subject: [PATCH] Procedural Prefabs: Add PhysX mesh group support and example (#6073) * Auto LOD script setup Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Working auto LODs Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Correctly selected LODs and added default prefab Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Cleanup code Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Cleanup code Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Add missing legal header, move name cleanup to scene_helpers, add documentation Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Add PhysX mesh group support. Updated example script to show usage Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Add a physics collider component Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Update DefaultOrValue call Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> --- .../Editor/Scripts/scene_mesh_to_prefab.py | 52 ++++ .../Editor/Scripts/scene_api/scene_data.py | 259 +++++++++++++++++- 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py index 6151585f26..db8df09091 100644 --- a/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py +++ b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py @@ -8,6 +8,7 @@ import azlmbr.bus import azlmbr.math +from scene_api.scene_data import PrimitiveShape, DecompositionMode from scene_helpers import * @@ -41,6 +42,35 @@ def add_material_component(entity_id): raise RuntimeError("UpdateComponentForEntity for editor_material_component failed") +def add_physx_meshes(scene_manifest: sceneData.SceneManifest, source_file_name: str, mesh_name_list: List, all_node_paths: List[str]): + first_mesh = mesh_name_list[0].get_path() + + # Add a Box Primitive PhysX mesh with a comment + physx_box = scene_manifest.add_physx_primitive_mesh_group(source_file_name + "_box", PrimitiveShape.BOX, 0.0, None) + scene_manifest.physx_mesh_group_add_comment(physx_box, "This is a box primitive") + # Select the first mesh, unselect every other node + scene_manifest.physx_mesh_group_add_selected_node(physx_box, first_mesh) + + for node in all_node_paths: + if node != first_mesh: + scene_manifest.physx_mesh_group_add_unselected_node(physx_box, node) + + # Add a Convex Mesh PhysX mesh with a comment + convex_mesh = scene_manifest.add_physx_convex_mesh_group(source_file_name + "_convex", 0.08, .0004, + True, True, True, True, True, 24, True, "Glass") + scene_manifest.physx_mesh_group_add_comment(convex_mesh, "This is a convex mesh") + # Select/Unselect nodes using lists + all_except_first_mesh = [x for x in all_node_paths if x != first_mesh] + scene_manifest.physx_mesh_group_add_selected_unselected_nodes(convex_mesh, [first_mesh], all_except_first_mesh) + + # Configure mesh decomposition for this mesh + scene_manifest.physx_mesh_group_decompose_meshes(convex_mesh, 512, 32, .002, 100100, DecompositionMode.TETRAHEDRON, + 0.06, 0.055, 0.00015, 3, 3, True, False) + + # Add a Triangle mesh + triangle = scene_manifest.add_physx_triangle_mesh_group(source_file_name + "_triangle", False, True, True, True, True, True) + scene_manifest.physx_mesh_group_add_selected_unselected_nodes(triangle, [first_mesh], all_except_first_mesh) + def update_manifest(scene): import uuid, os import azlmbr.scene.graph @@ -62,6 +92,8 @@ def update_manifest(scene): previous_entity_id = azlmbr.entity.InvalidEntityId first_mesh = True + add_physx_meshes(scene_manifest, source_filename_only, mesh_name_list, all_node_paths) + # Loop every mesh node in the scene for activeMeshIndex in range(len(mesh_name_list)): mesh_name = mesh_name_list[activeMeshIndex] @@ -106,6 +138,26 @@ def update_manifest(scene): if not result: raise RuntimeError("UpdateComponentForEntity failed for Mesh component") + # Add a physics component referencing the triangle mesh we made for the first node + if previous_entity_id is None: + physx_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", + entity_id, "{FD429282-A075-4966-857F-D0BBF186CFE6} EditorColliderComponent") + + json_update = json.dumps({ + "ShapeConfiguration": { + "PhysicsAsset": { + "Asset": { + "assetHint": os.path.join(source_relative_path, source_filename_only + "_triangle.pxmesh") + } + } + } + }) + + result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, physx_mesh_component, json_update) + + if not result: + raise RuntimeError("UpdateComponentForEntity failed for PhysX mesh component") + # an example of adding a material component to override the default material if previous_entity_id is not None and first_mesh: first_mesh = False diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py index d105ecdb43..173558d784 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py @@ -4,8 +4,11 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import azlmbr.scene as sceneApi +import typing import json +import azlmbr.scene as sceneApi +from enum import Enum, IntEnum + # Wraps the AZ.SceneAPI.Containers.SceneGraph.NodeIndex internal class class SceneGraphNodeIndex: @@ -24,6 +27,7 @@ class SceneGraphNodeIndex: def equal(self, other) -> bool: return self.nodeIndex.Equal(other) + # Wraps AZ.SceneAPI.Containers.SceneGraph.Name internal class class SceneGraphName(): def __init__(self, sceneGraphName) -> None: @@ -35,6 +39,7 @@ class SceneGraphName(): def get_name(self) -> str: return self.name.GetName() + # Wraps AZ.SceneAPI.Containers.SceneGraph class class SceneGraph(): def __init__(self, sceneGraphInstance) -> None: @@ -90,13 +95,26 @@ class SceneGraph(): def get_node_content(self, node): return self.sceneGraph.GetNodeContent(node) + +class PrimitiveShape(IntEnum): + BEST_FIT = 0 + SPHERE = 1 + BOX = 2 + CAPSULE = 3 + + +class DecompositionMode(IntEnum): + VOXEL = 0 + TETRAHEDRON = 1 + + # Contains a dictionary to contain and export AZ.SceneAPI.Containers.SceneManifest class SceneManifest(): def __init__(self): self.manifest = {'values': []} def add_mesh_group(self, name: str) -> dict: - meshGroup = {} + meshGroup = {} meshGroup['$type'] = '{07B356B7-3635-40B5-878A-FAC4EFD5AD86} MeshGroup' meshGroup['name'] = name meshGroup['nodeSelectionList'] = {'selectedNodes': [], 'unselectedNodes': []} @@ -272,5 +290,242 @@ class SceneManifest(): mesh_group['rules']['rules'].append(rule) + def __add_physx_base_mesh_group(self, name: str, physics_material: typing.Optional[str]) -> dict: + import azlmbr.math + group = { + '$type': '{5B03C8E6-8CEE-4DA0-A7FA-CD88689DD45B} MeshGroup', + 'id': azlmbr.math.Uuid_CreateRandom().ToString(), + 'name': name, + 'NodeSelectionList': { + 'selectedNodes': [], + 'unselectedNodes': [] + }, + "MaterialSlots": [ + "Material" + ], + "PhysicsMaterials": [ + self.__default_or_value(physics_material, "") + ], + "rules": { + "rules": [] + } + } + self.manifest['values'].append(group) + + return group + + def add_physx_triangle_mesh_group(self, name: str, merge_meshes: bool = True, weld_vertices: bool = False, + disable_clean_mesh: bool = False, + force_32bit_indices: bool = False, + suppress_triangle_mesh_remap_table: bool = False, + build_triangle_adjacencies: bool = False, + mesh_weld_tolerance: float = 0.0, + num_tris_per_leaf: int = 4, + physics_material: typing.Optional[str] = None) -> dict: + """ + Adds a Triangle type PhysX Mesh Group to the scene. + + :param name: Name of the mesh group. + :param merge_meshes: When true, all selected nodes will be merged into a single collision mesh. + :param weld_vertices: When true, mesh welding is performed. Clean mesh must be enabled. + :param disable_clean_mesh: When true, mesh cleaning is disabled. This makes cooking faster. + :param force_32bit_indices: When true, 32-bit indices will always be created regardless of triangle count. + :param suppress_triangle_mesh_remap_table: When true, the face remap table is not created. + This saves a significant amount of memory, but the SDK will not be able to provide the remap + information for internal mesh triangles returned by collisions, sweeps or raycasts hits. + :param build_triangle_adjacencies: When true, the triangle adjacency information is created. + :param mesh_weld_tolerance: If mesh welding is enabled, this controls the distance at + which vertices are welded. If mesh welding is not enabled, this value defines the + acceptance distance for mesh validation. Provided no two vertices are within this + distance, the mesh is considered to be clean. If not, a warning will be emitted. + :param num_tris_per_leaf: Mesh cooking hint for max triangles per leaf limit. Fewer triangles per leaf + produces larger meshes with better runtime performance and worse cooking performance. + :param physics_material: Configure which physics material to use. + :return: The newly created mesh group. + """ + group = self.__add_physx_base_mesh_group(name, physics_material) + group["export method"] = 0 + group["TriangleMeshAssetParams"] = { + "MergeMeshes": merge_meshes, + "WeldVertices": weld_vertices, + "DisableCleanMesh": disable_clean_mesh, + "Force32BitIndices": force_32bit_indices, + "SuppressTriangleMeshRemapTable": suppress_triangle_mesh_remap_table, + "BuildTriangleAdjacencies": build_triangle_adjacencies, + "MeshWeldTolerance": mesh_weld_tolerance, + "NumTrisPerLeaf": num_tris_per_leaf + } + + return group + + def add_physx_convex_mesh_group(self, name: str, area_test_epsilon: float = 0.059, plane_tolerance: float = 0.0006, + use_16bit_indices: bool = False, + check_zero_area_triangles: bool = False, + quantize_input: bool = False, + use_plane_shifting: bool = False, + shift_vertices: bool = False, + gauss_map_limit: int = 32, + build_gpu_data: bool = False, + physics_material: typing.Optional[str] = None) -> dict: + """ + Adds a Convex type PhysX Mesh Group to the scene. + + :param name: Name of the mesh group. + :param area_test_epsilon: If the area of a triangle of the hull is below this value, the triangle will be + rejected. This test is done only if Check Zero Area Triangles is used. + :param plane_tolerance: The value is used during hull construction. When a new point is about to be added + to the hull it gets dropped when the point is closer to the hull than the planeTolerance. + :param use_16bit_indices: Denotes the use of 16-bit vertex indices in Convex triangles or polygons. + :param check_zero_area_triangles: Checks and removes almost zero-area triangles during convex hull computation. + The rejected area size is specified in Area Test Epsilon. + :param quantize_input: Quantizes the input vertices using the k-means clustering. + :param use_plane_shifting: Enables plane shifting vertex limit algorithm. Plane shifting is an alternative + algorithm for the case when the computed hull has more vertices than the specified vertex + limit. + :param shift_vertices: Convex hull input vertices are shifted to be around origin to provide better + computation stability + :param gauss_map_limit: Vertex limit beyond which additional acceleration structures are computed for each + convex mesh. Increase that limit to reduce memory usage. Computing the extra structures + all the time does not guarantee optimal performance. + :param build_gpu_data: When true, additional information required for GPU-accelerated rigid body + simulation is created. This can increase memory usage and cooking times for convex meshes + and triangle meshes. Convex hulls are created with respect to GPU simulation limitations. + Vertex limit is set to 64 and vertex limit per face is internally set to 32. + :param physics_material: Configure which physics material to use. + :return: The newly created mesh group. + """ + group = self.__add_physx_base_mesh_group(name, physics_material) + group["export method"] = 1 + group["ConvexAssetParams"] = { + "AreaTestEpsilon": area_test_epsilon, + "PlaneTolerance": plane_tolerance, + "Use16bitIndices": use_16bit_indices, + "CheckZeroAreaTriangles": check_zero_area_triangles, + "QuantizeInput": quantize_input, + "UsePlaneShifting": use_plane_shifting, + "ShiftVertices": shift_vertices, + "GaussMapLimit": gauss_map_limit, + "BuildGpuData": build_gpu_data + } + + return group + + def add_physx_primitive_mesh_group(self, name: str, + primitive_shape_target: PrimitiveShape = PrimitiveShape.BEST_FIT, + volume_term_coefficient: float = 0.0, + physics_material: typing.Optional[str] = None) -> dict: + """ + Adds a Primitive Shape type PhysX Mesh Group to the scene + + :param name: Name of the mesh group. + :param primitive_shape_target: The shape that should be fitted to this mesh. If BEST_FIT is selected, the + algorithm will determine which of the shapes fits best. + :param volume_term_coefficient: This parameter controls how aggressively the primitive fitting algorithm will try + to minimize the volume of the fitted primitive. A value of 0 (no volume minimization) is + recommended for most meshes, especially those with moderate to high vertex counts. + :param physics_material: Configure which physics material to use. + :return: The newly created mesh group. + """ + group = self.__add_physx_base_mesh_group(name, physics_material) + group["export method"] = 2 + group["PrimitiveAssetParams"] = { + "PrimitiveShapeTarget": int(primitive_shape_target), + "VolumeTermCoefficient": volume_term_coefficient + } + + return group + + def physx_mesh_group_decompose_meshes(self, mesh_group: dict, max_convex_hulls: int = 1024, + max_num_vertices_per_convex_hull: int = 64, + concavity: float = .001, + resolution: float = 100000, + mode: DecompositionMode = DecompositionMode.VOXEL, + alpha: float = .05, + beta: float = .05, + min_volume_per_convex_hull: float = 0.0001, + plane_downsampling: int = 4, + convex_hull_downsampling: int = 4, + pca: bool = False, + project_hull_vertices: bool = True) -> None: + """ + Enables and configures mesh decomposition for a PhysX Mesh Group. + Only valid for convex or primitive mesh types. + + :param mesh_group: Mesh group to configure decomposition for. + :param max_convex_hulls: Controls the maximum number of hulls to generate. + :param max_num_vertices_per_convex_hull: Controls the maximum number of triangles per convex hull. + :param concavity: Maximum concavity of each approximate convex hull. + :param resolution: Maximum number of voxels generated during the voxelization stage. + :param mode: Select voxel-based approximate convex decomposition or tetrahedron-based + approximate convex decomposition. + :param alpha: Controls the bias toward clipping along symmetry planes. + :param beta: Controls the bias toward clipping along revolution axes. + :param min_volume_per_convex_hull: Controls the adaptive sampling of the generated convex hulls. + :param plane_downsampling: Controls the granularity of the search for the best clipping plane. + :param convex_hull_downsampling: Controls the precision of the convex hull generation process + during the clipping plane selection stage. + :param pca: Enable or disable normalizing the mesh before applying the convex decomposition. + :param project_hull_vertices: Project the output convex hull vertices onto the original source mesh to increase + the floating point accuracy of the results. + """ + mesh_group['DecomposeMeshes'] = True + mesh_group['ConvexDecompositionParams'] = { + "MaxConvexHulls": max_convex_hulls, + "MaxNumVerticesPerConvexHull": max_num_vertices_per_convex_hull, + "Concavity": concavity, + "Resolution": resolution, + "Mode": int(mode), + "Alpha": alpha, + "Beta": beta, + "MinVolumePerConvexHull": min_volume_per_convex_hull, + "PlaneDownsampling": plane_downsampling, + "ConvexHullDownsampling": convex_hull_downsampling, + "PCA": pca, + "ProjectHullVertices": project_hull_vertices + } + + def physx_mesh_group_add_selected_node(self, mesh_group: dict, node: str) -> None: + """ + Adds a node to the selected nodes list + + :param mesh_group: Mesh group to add to. + :param node: Node path to add. + """ + mesh_group['NodeSelectionList']['selectedNodes'].append(node) + + def physx_mesh_group_add_unselected_node(self, mesh_group: dict, node: str) -> None: + """ + Adds a node to the unselected nodes list + + :param mesh_group: Mesh group to add to. + :param node: Node path to add. + """ + mesh_group['NodeSelectionList']['unselectedNodes'].append(node) + + def physx_mesh_group_add_selected_unselected_nodes(self, mesh_group: dict, selected: typing.List[str], + unselected: typing.List[str]) -> None: + """ + Adds a set of nodes to the selected/unselected node lists + + :param mesh_group: Mesh group to add to. + :param selected: List of node paths to add to the selected list. + :param unselected: List of node paths to add to the unselected list. + """ + mesh_group['NodeSelectionList']['selectedNodes'].extend(selected) + mesh_group['NodeSelectionList']['unselectedNodes'].extend(unselected) + + def physx_mesh_group_add_comment(self, mesh_group: dict, comment: str) -> None: + """ + Adds a comment rule + + :param mesh_group: Mesh group to add the rule to. + :param comment: Comment string. + """ + rule = { + "$type": "CommentRule", + "comment": comment + } + mesh_group['rules']['rules'].append(rule) + def export(self): return json.dumps(self.manifest)