Pārlūkot izejas kodu

Procedural Prefabs: Add example LOD script (#6057)

* Auto LOD script setup

Signed-off-by: amzn-mike <[email protected]>

* Working auto LODs

Signed-off-by: amzn-mike <[email protected]>

* Correctly selected LODs and added default prefab

Signed-off-by: amzn-mike <[email protected]>

* Cleanup code

Signed-off-by: amzn-mike <[email protected]>

* Cleanup code

Signed-off-by: amzn-mike <[email protected]>

* Add missing legal header, move name cleanup to scene_helpers, add documentation

Signed-off-by: amzn-mike <[email protected]>
amzn-mike 3 gadi atpakaļ
vecāks
revīzija
f97ec14cf8

+ 124 - 0
AutomatedTesting/Editor/Scripts/auto_lod.py

@@ -0,0 +1,124 @@
+#
+# 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 os, traceback, binascii, sys, json, pathlib, logging
+import azlmbr.math
+import azlmbr.bus
+from scene_helpers import *
+
+#
+# SceneAPI Processor
+#
+
+def update_manifest(scene):
+    import uuid
+    import azlmbr.scene as sceneApi
+    import azlmbr.scene.graph
+    from scene_api import scene_data as sceneData
+
+    graph = sceneData.SceneGraph(scene.graph)
+    # Get a list of all the mesh nodes, as well as all the nodes
+    mesh_name_list, all_node_paths = get_mesh_node_names(graph)
+    mesh_name_list.sort(key=lambda node: str.casefold(node.get_path()))
+    scene_manifest = sceneData.SceneManifest()
+
+    clean_filename = scene.sourceFilename.replace('.', '_')
+
+    # Compute the filename of the scene file
+    source_basepath = scene.watchFolder
+    source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
+    source_filename_only = os.path.basename(clean_filename)
+
+    created_entities = []
+    previous_entity_id = azlmbr.entity.InvalidEntityId
+    first_mesh = True
+
+    # Make a list of mesh node paths
+    mesh_path_list = list(map(lambda node: node.get_path(), mesh_name_list))
+
+    # Assume the first mesh is the main mesh
+    main_mesh = mesh_name_list[0]
+    mesh_path = main_mesh.get_path()
+
+    # Create a unique mesh group name using the filename + node name
+    mesh_group_name = '{}_{}'.format(source_filename_only, main_mesh.get_name())
+    # Remove forbidden filename characters from the name since this will become a file on disk later
+    mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
+    # Add the MeshGroup to the manifest and give it a unique ID
+    mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
+    mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
+    # Set our current node as the only node that is included in this MeshGroup
+    scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
+
+    # Explicitly remove all other nodes to prevent implicit inclusions
+    for node in mesh_path_list:
+        if node != mesh_path:
+            scene_manifest.mesh_group_unselect_node(mesh_group, node)
+
+    # Create a LOD rule
+    lod_rule = scene_manifest.mesh_group_add_lod_rule(mesh_group)
+
+    # Loop all the mesh nodes after the first
+    for x in mesh_path_list[1:]:
+        # Add a new LOD level
+        lod = scene_manifest.lod_rule_add_lod(lod_rule)
+        # Select the current mesh for this LOD level
+        scene_manifest.lod_select_node(lod, x)
+
+        # Unselect every other mesh for this LOD level
+        for y in mesh_path_list:
+            if y != x:
+                scene_manifest.lod_unselect_node(lod, y)
+
+    # Create an editor entity
+    entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
+    # Add an EditorMeshComponent to the entity
+    editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
+    # Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
+    # The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
+    # The assetHint will be converted to an AssetId later during prefab loading
+    json_update = json.dumps({
+        "Controller": { "Configuration": { "ModelAsset": {
+            "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
+        });
+    # Apply the JSON above to the component we created
+    result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
+
+    if not result:
+        raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
+
+    create_prefab(scene_manifest, source_filename_only, [entity_id])
+
+    # Convert the manifest to a JSON string and return it
+    new_manifest = scene_manifest.export()
+
+    return new_manifest
+
+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 = None
+
+# try to create SceneAPI handler for processing
+try:
+    import azlmbr.scene as sceneApi
+    if (sceneJobHandler == None):
+        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+        sceneJobHandler.connect()
+        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+except:
+    sceneJobHandler = None

+ 95 - 0
AutomatedTesting/Editor/Scripts/scene_helpers.py

@@ -0,0 +1,95 @@
+"""
+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
+
+import azlmbr.bus
+from scene_api import scene_data as sceneData
+from scene_api.scene_data import SceneGraphName
+
+
+def log_exception_traceback():
+    """
+    Outputs an exception stacktrace.
+    """
+    data = traceback.format_exc()
+    logger = logging.getLogger('python')
+    logger.error(data)
+
+
+def sanitize_name_for_disk(name: str):
+    """
+    Removes illegal filename characters from a string.
+
+    :param name: String to clean.
+    :return: Name with illegal characters removed.
+    """
+    return "".join(char for char in name if char not in "|<>:\"/?*\\")
+
+
+def get_mesh_node_names(scene_graph: sceneData.SceneGraph) -> Tuple[List[SceneGraphName], List[str]]:
+    """
+    Returns a tuple of all the mesh nodes as well as all the node paths
+
+    :param scene_graph: Scene graph to search
+    :return: Tuple of [Mesh Nodes, All Node Paths]
+    """
+    import azlmbr.scene as sceneApi
+    import azlmbr.scene.graph
+
+    mesh_data_list = []
+    node = scene_graph.get_root()
+    children = []
+    paths = []
+
+    while node.IsValid():
+        # store children to process after siblings
+        if scene_graph.has_node_child(node):
+            children.append(scene_graph.get_node_child(node))
+
+        node_name = sceneData.SceneGraphName(scene_graph.get_node_name(node))
+        paths.append(node_name.get_path())
+
+        # store any node that has mesh data content
+        node_content = scene_graph.get_node_content(node)
+        if node_content.CastWithTypeName('MeshData'):
+            if scene_graph.is_node_end_point(node) is False:
+                if len(node_name.get_path()):
+                    mesh_data_list.append(sceneData.SceneGraphName(scene_graph.get_node_name(node)))
+
+        # advance to next node
+        if scene_graph.has_node_sibling(node):
+            node = scene_graph.get_node_sibling(node)
+        elif children:
+            node = children.pop()
+        else:
+            node = azlmbr.scene.graph.NodeIndex()
+
+    return mesh_data_list, paths
+
+
+def create_prefab(scene_manifest: sceneData.SceneManifest, prefab_name: str, entities: list) -> None:
+    prefab_filename = prefab_name + ".prefab"
+    created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", entities,
+                                                                 prefab_filename)
+
+    if created_template_id is None or created_template_id == azlmbr.prefab.InvalidTemplateId:
+        raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
+
+    # Convert the prefab to a JSON string
+    output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
+
+    if output is not None and output.IsSuccess():
+        json_string = output.GetValue()
+        uuid = azlmbr.math.Uuid_CreateRandom().ToString()
+        json_result = json.loads(json_string)
+        # Add a PrefabGroup to the manifest and store the JSON on it
+        scene_manifest.add_prefab_group(prefab_name, uuid, json_result)
+    else:
+        raise RuntimeError(
+            "SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))

+ 42 - 89
AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py

@@ -5,55 +5,16 @@
 # SPDX-License-Identifier: Apache-2.0 OR MIT
 #
 #
-import os, traceback, binascii, sys, json, pathlib, logging
-import azlmbr.math
 import azlmbr.bus
+import azlmbr.math
+
+from scene_helpers import *
+
 
 #
 # SceneAPI Processor
 #
 
-
-def log_exception_traceback():
-    data = traceback.format_exc()
-    logger = logging.getLogger('python')
-    logger.error(data)
-
-def get_mesh_node_names(sceneGraph):
-    import azlmbr.scene as sceneApi
-    import azlmbr.scene.graph
-    from scene_api import scene_data as sceneData
-
-    meshDataList = []
-    node = sceneGraph.get_root()
-    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 = sceneData.SceneGraphName(sceneGraph.get_node_name(node))
-        paths.append(nodeName.get_path())
-
-        # store any node that has mesh data content
-        nodeContent = sceneGraph.get_node_content(node)
-        if nodeContent.CastWithTypeName('MeshData'):
-            if sceneGraph.is_node_end_point(node) is False:
-                if (len(nodeName.get_path())):
-                    meshDataList.append(sceneData.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 meshDataList, paths
-
 def add_material_component(entity_id):
     # Create an override AZ::Render::EditorMaterialComponent
     editor_material_component = azlmbr.entity.EntityUtilityBus(
@@ -64,24 +25,24 @@ def add_material_component(entity_id):
 
     # this fills out the material asset to a known product AZMaterial asset relative path
     json_update = json.dumps({
-            "Controller": { "Configuration": { "materials": [
-                {
-                    "Key": {},
-                    "Value": { "MaterialAsset":{
-                        "assetHint": "materials/basic_grey.azmaterial"
-                    }}
-                }]
-            }}
-        });
-    result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_material_component, json_update)
+        "Controller": {"Configuration": {"materials": [
+            {
+                "Key": {},
+                "Value": {"MaterialAsset": {
+                    "assetHint": "materials/basic_grey.azmaterial"
+                }}
+            }]
+        }}
+    })
+    result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
+                                            editor_material_component, json_update)
 
     if not result:
         raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
 
+
 def update_manifest(scene):
-    import json
     import uuid, os
-    import azlmbr.scene as sceneApi
     import azlmbr.scene.graph
     from scene_api import scene_data as sceneData
 
@@ -89,9 +50,9 @@ def update_manifest(scene):
     # Get a list of all the mesh nodes, as well as all the nodes
     mesh_name_list, all_node_paths = get_mesh_node_names(graph)
     scene_manifest = sceneData.SceneManifest()
-    
+
     clean_filename = scene.sourceFilename.replace('.', '_')
-    
+
     # Compute the filename of the scene file
     source_basepath = scene.watchFolder
     source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
@@ -108,7 +69,7 @@ def update_manifest(scene):
         # Create a unique mesh group name using the filename + node name
         mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
         # Remove forbidden filename characters from the name since this will become a file on disk later
-        mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
+        mesh_group_name = sanitize_name_for_disk(mesh_group_name)
         # Add the MeshGroup to the manifest and give it a unique ID
         mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
         mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
@@ -129,16 +90,18 @@ def update_manifest(scene):
         # Create an editor entity
         entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
         # Add an EditorMeshComponent to the entity
-        editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
-        # Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
-        # The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
-        # The assetHint will be converted to an AssetId later during prefab loading
+        editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
+                                                               entity_id, "AZ::Render::EditorMeshComponent")
+        # Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just
+        # created + the azmodel extension The MeshGroup we created will be output as a product in the asset's path
+        # named mesh_group_name.azmodel The assetHint will be converted to an AssetId later during prefab loading
         json_update = json.dumps({
-            "Controller": { "Configuration": { "ModelAsset": {
-                "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
-            });
+            "Controller": {"Configuration": {"ModelAsset": {
+                "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel"}}}
+        })
         # Apply the JSON above to the component we created
-        result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
+        result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
+                                                editor_mesh_component, json_update)
 
         if not result:
             raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
@@ -149,17 +112,19 @@ def update_manifest(scene):
             add_material_component(entity_id)
 
         # Get the transform component
-        transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
+        transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
+                                                             entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
 
         # Set this entity to be a child of the last entity we created
         # This is just an example of how to do parenting and isn't necessarily useful to parent everything like this
         if previous_entity_id is not None:
             transform_json = json.dumps({
-                "Parent Entity" : previous_entity_id.to_json()
-                });
+                "Parent Entity": previous_entity_id.to_json()
+            })
 
             # Apply the JSON update
-            result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, transform_component, transform_json)
+            result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
+                                                    transform_component, transform_json)
 
             if not result:
                 raise RuntimeError("UpdateComponentForEntity failed for Transform component")
@@ -171,37 +136,23 @@ def update_manifest(scene):
         created_entities.append(entity_id)
 
     # Create a prefab with all our entities
-    prefab_filename = source_filename_only + ".prefab"
-    created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", created_entities, prefab_filename)
-
-    if created_template_id == azlmbr.prefab.InvalidTemplateId:
-        raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
-
-    # Convert the prefab to a JSON string
-    output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
-
-    if output.IsSuccess():
-        jsonString = output.GetValue()
-        uuid = azlmbr.math.Uuid_CreateRandom().ToString()
-        jsonResult = json.loads(jsonString)
-        # Add a PrefabGroup to the manifest and store the JSON on it
-        scene_manifest.add_prefab_group(source_filename_only, uuid, jsonResult)
-    else:
-        raise RuntimeError("SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
+    create_prefab(scene_manifest, source_filename_only, created_entities)
 
     # Convert the manifest to a JSON string and return it
     new_manifest = scene_manifest.export()
 
     return new_manifest
 
+
 sceneJobHandler = None
 
+
 def on_update_manifest(args):
     try:
         scene = args[0]
         return update_manifest(scene)
     except RuntimeError as err:
-        print (f'ERROR - {err}')
+        print(f'ERROR - {err}')
         log_exception_traceback()
     except:
         log_exception_traceback()
@@ -209,10 +160,12 @@ def on_update_manifest(args):
     global sceneJobHandler
     sceneJobHandler = None
 
+
 # try to create SceneAPI handler for processing
 try:
     import azlmbr.scene as sceneApi
-    if (sceneJobHandler == None):
+
+    if sceneJobHandler is None:
         sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
         sceneJobHandler.connect()
         sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)

+ 3 - 0
AutomatedTesting/Objects/sphere_5lods.fbx

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e169277bca473325281d5fe043cffc9196bd3ef46f6bffbea6e0b5e3b7194a1
+size 62700

+ 8 - 0
AutomatedTesting/Objects/sphere_5lods.fbx.assetinfo

@@ -0,0 +1,8 @@
+{
+    "values": [
+        {
+            "$type": "ScriptProcessorRule",
+            "scriptFilename": "Editor/Scripts/auto_lod.py"
+        }
+    ]
+}