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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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, "<Default 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)
|
||||
|
||||
Reference in New Issue
Block a user