scene_mesh_to_prefab.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import azlmbr.bus
  9. import azlmbr.math
  10. from scene_api.scene_data import PrimitiveShape, DecompositionMode, ColorChannel, TangentSpaceSource, TangentSpaceMethod
  11. from scene_helpers import *
  12. #
  13. # SceneAPI Processor
  14. #
  15. def add_material_component(entity_id):
  16. # Create an override AZ::Render::EditorMaterialComponent
  17. editor_material_component = azlmbr.entity.EntityUtilityBus(
  18. azlmbr.bus.Broadcast,
  19. "GetOrAddComponentByTypeName",
  20. entity_id,
  21. "EditorMaterialComponent")
  22. # this fills out the material asset to a known product AZMaterial asset relative path
  23. json_update = json.dumps({
  24. "Controller": {"Configuration": {"materials": [
  25. {
  26. "Key": {},
  27. "Value": {"MaterialAsset": {
  28. "assetHint": "materials/basic_grey.azmaterial"
  29. }}
  30. }]
  31. }}
  32. })
  33. result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
  34. editor_material_component, json_update)
  35. if not result:
  36. raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
  37. def add_physx_meshes(scene_manifest: sceneData.SceneManifest, source_file_name: str, mesh_name_list: List, all_node_paths: List[str]):
  38. import uuid
  39. first_mesh = mesh_name_list[0].get_path()
  40. # Add a Box Primitive PhysX mesh with a comment
  41. physx_box_group = scene_manifest.add_physx_primitive_mesh_group(source_file_name + "_box", PrimitiveShape.BOX, 0.0, None)
  42. # Give a unique ID the PhysX mesh group
  43. physx_box_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_file_name + "_box")) + '}'
  44. scene_manifest.physx_mesh_group_add_comment(physx_box_group, "This is a box primitive")
  45. # Select the first mesh, unselect every other node
  46. scene_manifest.physx_mesh_group_add_selected_node(physx_box_group, first_mesh)
  47. for node in all_node_paths:
  48. if node != first_mesh:
  49. scene_manifest.physx_mesh_group_add_unselected_node(physx_box_group, node)
  50. # Add a Convex Mesh PhysX mesh with a comment
  51. physx_convex_mesh_group = scene_manifest.add_physx_convex_mesh_group(source_file_name + "_convex",
  52. area_test_epsilon = 0.08,
  53. plane_tolerance = .0004,
  54. use_16bit_indices = True,
  55. check_zero_area_triangles = True,
  56. quantize_input = True,
  57. use_plane_shifting = True,
  58. shift_vertices = True,
  59. gauss_map_limit = 24,
  60. build_gpu_data = True,
  61. physics_material_asset_hint = "physx/glass.physicsmaterial")
  62. # Give a unique ID the PhysX mesh group
  63. physx_convex_mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_file_name + "_convex")) + '}'
  64. scene_manifest.physx_mesh_group_add_comment(physx_convex_mesh_group, "This is a convex mesh")
  65. # Select/Unselect nodes using lists
  66. all_except_first_mesh = [x for x in all_node_paths if x != first_mesh]
  67. scene_manifest.physx_mesh_group_add_selected_unselected_nodes(physx_convex_mesh_group, [first_mesh], all_except_first_mesh)
  68. # Configure mesh decomposition for this mesh
  69. scene_manifest.physx_mesh_group_decompose_meshes(physx_convex_mesh_group, 512, 32, .002, 100100, DecompositionMode.TETRAHEDRON,
  70. 0.06, 0.055, 0.00015, 3, 3, True, False)
  71. # Add a Triangle mesh
  72. physx_triangle_mesh_group = scene_manifest.add_physx_triangle_mesh_group(source_file_name + "_triangle",
  73. merge_meshes = False,
  74. weld_vertices = True,
  75. disable_clean_mesh = True,
  76. force_32bit_indices = True,
  77. suppress_triangle_mesh_remap_table = True,
  78. build_triangle_adjacencies = True)
  79. # Give a unique ID the PhysX mesh group
  80. physx_triangle_mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_file_name + "_triangle")) + '}'
  81. scene_manifest.physx_mesh_group_add_selected_unselected_nodes(physx_triangle_mesh_group, [first_mesh], all_except_first_mesh)
  82. def update_manifest(scene):
  83. import uuid, os
  84. import azlmbr.scene.graph
  85. from scene_api import scene_data as sceneData
  86. graph = sceneData.SceneGraph(scene.graph)
  87. # Get a list of all the mesh nodes, as well as all the nodes
  88. mesh_name_list, all_node_paths = get_mesh_node_names(graph)
  89. scene_manifest = sceneData.SceneManifest()
  90. clean_filename = scene.sourceFilename.replace('.', '_')
  91. # Compute the filename of the scene file
  92. source_basepath = scene.watchFolder
  93. source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
  94. source_filename_only = os.path.basename(clean_filename)
  95. created_entities = []
  96. previous_entity_id = azlmbr.entity.InvalidEntityId
  97. first_mesh = True
  98. add_physx_meshes(scene_manifest, source_filename_only, mesh_name_list, all_node_paths)
  99. # Loop every mesh node in the scene
  100. for activeMeshIndex in range(len(mesh_name_list)):
  101. mesh_name = mesh_name_list[activeMeshIndex]
  102. mesh_path = mesh_name.get_path()
  103. # Create a unique mesh group name using the filename + node name
  104. mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
  105. # Remove forbidden filename characters from the name since this will become a file on disk later
  106. mesh_group_name = sanitize_name_for_disk(mesh_group_name)
  107. # Add the MeshGroup to the manifest and give it a unique ID
  108. mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
  109. mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
  110. # Set our current node as the only node that is included in this MeshGroup
  111. scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
  112. scene_manifest.mesh_group_add_comment(mesh_group, "Hello World")
  113. # Explicitly remove all other nodes to prevent implicit inclusions
  114. for node in all_node_paths:
  115. if node != mesh_path:
  116. scene_manifest.mesh_group_unselect_node(mesh_group, node)
  117. scene_manifest.mesh_group_add_cloth_rule(mesh_group, mesh_path, "Col0", ColorChannel.GREEN, "Col0",
  118. ColorChannel.BLUE, "Col0", ColorChannel.BLUE, ColorChannel.ALPHA)
  119. scene_manifest.mesh_group_add_advanced_mesh_rule(mesh_group, True, False, True, "Col0")
  120. scene_manifest.mesh_group_add_skin_rule(mesh_group, 3, 0.002)
  121. scene_manifest.mesh_group_add_tangent_rule(mesh_group, TangentSpaceSource.MIKKT_GENERATION, TangentSpaceMethod.TSPACE_BASIC)
  122. # Create an editor entity
  123. entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
  124. # Add an EditorMeshComponent to the entity
  125. editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
  126. entity_id, "AZ::Render::EditorMeshComponent")
  127. # Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just
  128. # created + the azmodel extension The MeshGroup we created will be output as a product in the asset's path
  129. # named mesh_group_name.fbx.azmodel The assetHint will be converted to an AssetId later during prefab loading
  130. json_update = json.dumps({
  131. "Controller": {"Configuration": {"ModelAsset": {
  132. "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".fbx.azmodel"}}}
  133. })
  134. # Apply the JSON above to the component we created
  135. result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
  136. editor_mesh_component, json_update)
  137. if not result:
  138. raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
  139. # Add a physics component referencing the triangle mesh we made for the first node
  140. if previous_entity_id is None:
  141. physx_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
  142. entity_id, "{20382794-0E74-4860-9C35-A19F22DC80D4} EditorMeshColliderComponent")
  143. json_update = json.dumps({
  144. "ShapeConfiguration": {
  145. "PhysicsAsset": {
  146. "Asset": {
  147. "assetHint": os.path.join(source_relative_path, source_filename_only + "_triangle.fbx.pxmesh")
  148. }
  149. }
  150. }
  151. })
  152. result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, physx_mesh_component, json_update)
  153. if not result:
  154. raise RuntimeError("UpdateComponentForEntity failed for PhysX mesh component")
  155. # an example of adding a material component to override the default material
  156. if previous_entity_id is not None and first_mesh:
  157. first_mesh = False
  158. add_material_component(entity_id)
  159. # Get the transform component
  160. transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
  161. entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
  162. # Set this entity to be a child of the last entity we created
  163. # This is just an example of how to do parenting and isn't necessarily useful to parent everything like this
  164. if previous_entity_id is not None:
  165. transform_json = json.dumps({
  166. "Parent Entity": previous_entity_id.to_json()
  167. })
  168. # Apply the JSON update
  169. result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
  170. transform_component, transform_json)
  171. if not result:
  172. raise RuntimeError("UpdateComponentForEntity failed for Transform component")
  173. # Update the last entity id for next time
  174. previous_entity_id = entity_id
  175. # Keep track of the entity we set up, we'll add them all to the prefab we're creating later
  176. created_entities.append(entity_id)
  177. # Create a prefab with all our entities
  178. create_prefab(scene_manifest, source_filename_only, created_entities)
  179. # Convert the manifest to a JSON string and return it
  180. new_manifest = scene_manifest.export()
  181. return new_manifest
  182. sceneJobHandler = None
  183. def on_update_manifest(args):
  184. try:
  185. scene = args[0]
  186. data = update_manifest(scene)
  187. except RuntimeError as err:
  188. print(f'ERROR - {err}')
  189. log_exception_traceback()
  190. except:
  191. log_exception_traceback()
  192. global sceneJobHandler
  193. # do not delete or set sceneJobHandler to None, just disconnect from it.
  194. # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
  195. # would cause a crash.
  196. sceneJobHandler.disconnect()
  197. return data
  198. # try to create SceneAPI handler for processing
  199. try:
  200. import azlmbr.scene as sceneApi
  201. sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
  202. sceneJobHandler.connect()
  203. sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
  204. except:
  205. sceneJobHandler = None