Aggregate StaticMeshActors in Unreal Engine using the Simplygon Subsystem and Python

Written by Jesper Tingvall, Product Expert, Simplygon

Disclaimer: The code in this post is written using version 10.4.117.0 of Simplygon and Unreal Engine 5.4. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.

Introduction

In this blog we will have a first look at the new Simplygon Subsystem import and export API using Python for Unreal Engine. We will use it to aggregate and remove internal geometry of a collection of Static Mesh Actors.

Problem to solve

We want to aggregate a collection of Static Mesh Actors in our world and create a new Static Mesh containing all of them. Our collection of meshes contains several triangles inside the model causing overdraw, we want to cull away these.

Car crash consisting of a buss and several rocks

This would normally be done with a Stand-in Actor. But in this example we want to showcase how to do it with our new import-export API.

Prerequisites

This example will use the Simplygon integration in Unreal Engine 5.4. The same concepts can be applied to all other integrations of the Simplygon API.

Solution

We will use the new Simplygon Subsystem introduced in Simplygon 10.4. With it, we can export Actors from Unreal Engine as Simplygon scenes and import them again. This allows us to use the full power of the Simplygon API without being restricted to what the user interface provides us with.

The Subsystem API is accessible to C++, blueprint and Python. We will use Python in this example, with a little bit of blueprint to get it into the UI. At the time of writing this blog post, the Simplygon subsystem is very new. Please advice the Limitations and caveats with the Simplygon Subsystem import / export methods .

After exporting it from Unreal we will use an aggregation pipeline to merge the geometries. We start with creating a script named aggregation.py inside <project root>/Content/Python folder.

Export from Unreal Engine

We will start with a function for exporting selected Static Mesh Actors using the python version of the Simplygon Subystem API. This code is very similar as to what can be found in the Python examples.

def export_from_unreal(export_file_path):
    simplygon_subsystem = unreal.get_editor_subsystem(unreal.SimplygonSubsystem)

    # Get actors to export.
    EAS = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    selected_actors = EAS.get_selected_level_actors()

    # Set export settings to use.
    export_settings = unreal.SgSceneExportSettings()
    export_settings.overwrite_existing_file = True

    # We will not bake materials so do not need to export them. This will speed up export time.
    export_settings.material_export_settings.export_baked_materials = False

    # Call export function.
    export_task_id = simplygon_subsystem.export_actors_to_scene(export_file_path, selected_actors, export_settings)

    # Fetch the result (which will wait for the result).
    export_result = simplygon_subsystem.retrieve_export_actors_to_scene_result(export_task_id)

    # Log result.
    if export_result.has_error:
        unreal.log_error(f"Export error: {export_result.error_message}")
    else:
        unreal.log(
            f"Export result: Scene written to file {export_result.output_file_path}"
        )

    return export_result

Aggregation pipeline

Once we have the Simplygon scene exported from Unreal Engine we can process it with the Simplygon API. This unlocks a lot of scripting potential, making it possible to perform operations not possible before in Unreal Engine. But for now let us focus on a simple use case.

We will use an aggregation pipeline to merge all meshes into one. To remove internal triangles we set EnableGeometryCulling to True.

def aggregate_scene(sg: Simplygon.ISimplygon, input_file: str, output_file: str, culling_precision: float) -> None:
    sg_aggregation_pipeline = sg.CreateAggregationPipeline()
    sg_aggregation_pipeline_settings = sg_aggregation_pipeline.GetAggregationSettings()

    # Merge all geometries into a single geometry and perform geometry culling.
    sg_aggregation_pipeline_settings.SetMergeGeometries(True)
    sg_aggregation_pipeline_settings.SetEnableGeometryCulling(True)
    sg_aggregation_pipeline_settings.SetGeometryCullingPrecision(culling_precision)

    sg_aggregation_pipeline.RunSceneFromFile(input_file, output_file, Simplygon.EPipelineRunMode_RunInNewProcess)

Import to Unreal Engine

Importing into Unreal is also handled with the Simplygon subsystem. This function loads a Simplygon scene into the current world.

def import_to_world(import_scene_path):
    simplygon_subsystem = unreal.get_editor_subsystem(unreal.SimplygonSubsystem)

    # Get world to spawn imported actors into.
    editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
    world = editor_subsystem.get_editor_world()

    # Set import settings to use.
    import_settings = unreal.SgSceneImportSettings()

    # Call import function
    import_task_id = simplygon_subsystem.import_scene_as_actors(import_scene_path, world, import_settings)

    # Fetch the result (which will wait for the result).
    import_result = simplygon_subsystem.retrieve_import_scene_as_actors_result(import_task_id)

    # Log result.
    if import_result.has_error:
        unreal.log_error(f"Import error: {import_result.error_message}")
    else:
        unreal.log( f"Import result: Num Actors imported {len(import_result.imported_actor_paths)}")

    return import_result

Main function and progress bar

We will now put our export, aggregate and import functions together. To make it clear to the user that we are processing the scene we put our code in a unreal.ScopedSlowTask scope. This will bring up a nice progress bar during processing.

def main():
    export_file_path = unreal.Paths.project_intermediate_dir() + "/Simplygon/" + "exported_scene.sgscene"
    processed_file_path = unreal.Paths.project_intermediate_dir() + "/Simplygon/" + "processed_scene.sgscene"

    with unreal.ScopedSlowTask(3, "Export from Unreal") as slow_task:
        slow_task.make_dialog(True)

        slow_task.enter_progress_frame(1, "Export from Unreal")
        export_from_unreal(export_file_path)

        sg = simplygon_loader.init_simplygon()
        slow_task.enter_progress_frame(1, "Perform aggregation")
        aggregate_scene(sg, export_file_path, processed_file_path, 0.5)

        slow_task.enter_progress_frame(1, "Import into Unreal")
        import_to_world(processed_file_path)

Editor utility blueprint

Now we have a script which performs our desired operation. The question now is how to run it from within the Unreal editor in a user friendly way.

We will start by creating a new blueprint that allows us to easily use our script from a right click menu. Create a new Blueprint of type ActorActionUtility.

Create a Blueprint of type ActorActionUtility

To this newly created blueprint we add a function, Merge Selected Actors, which has an Execute Python Command node with our python script aggregate.py specified.

Merge Selected actor function

Result

We select the Static Mesh Actors in the world we want to aggregate, then right click → Scripted Actor Actions → Merge Selected Actors.

Right click selected actors → Scripted Actor Actions → Merge Selected Actors

After processing we get a new static mesh actor in our scene.

Result, merged asset compared to original asset

If we inspect the inside of our output mesh we can see that internal geometry is now removed.

Original
Aggregation with geometry culling

Complete script

This script should be placed inside <project root>/Content/Python.

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import unreal
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon

def export_from_unreal(export_file_path):
    simplygon_subsystem = unreal.get_editor_subsystem(unreal.SimplygonSubsystem)

    # Get actors to export.
    EAS = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    selected_actors = EAS.get_selected_level_actors()

    # Set export settings to use.
    export_settings = unreal.SgSceneExportSettings()
    export_settings.overwrite_existing_file = True

    # We will not bake materials so do not need to export them. This will speed up export time.
    export_settings.material_export_settings.export_baked_materials = False

    # Call export function.
    export_task_id = simplygon_subsystem.export_actors_to_scene(export_file_path, selected_actors, export_settings)

    # Fetch the result (which will wait for the result).
    export_result = simplygon_subsystem.retrieve_export_actors_to_scene_result(export_task_id)

    # Log result.
    if export_result.has_error:
        unreal.log_error(f"Export error: {export_result.error_message}")
    else:
        unreal.log(
            f"Export result: Scene written to file {export_result.output_file_path}"
        )

    return export_result


def import_to_world(import_scene_path, tex_coord_name=""):
    simplygon_subsystem = unreal.get_editor_subsystem(unreal.SimplygonSubsystem)

    # Get world to spawn imported actors into.
    editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
    world = editor_subsystem.get_editor_world()

    # Set import settings to use.
    import_settings = unreal.SgSceneImportSettings()

    # Call import function
    import_task_id = simplygon_subsystem.import_scene_as_actors(import_scene_path, world, import_settings)

    # Fetch the result (which will wait for the result).
    import_result = simplygon_subsystem.retrieve_import_scene_as_actors_result(import_task_id)

    # Log result.
    if import_result.has_error:
        unreal.log_error(f"Import error: {import_result.error_message}")
    else:
        unreal.log( f"Import result: Num Actors imported {len(import_result.imported_actor_paths)}")

    return import_result


def aggregate_scene(sg: Simplygon.ISimplygon, input_file: str, output_file: str, culling_precision: float) -> None:
    sg_aggregation_pipeline = sg.CreateAggregationPipeline()
    sg_aggregation_pipeline_settings = sg_aggregation_pipeline.GetAggregationSettings()

    # Merge all geometries into a single geometry and perform geometry culling.
    sg_aggregation_pipeline_settings.SetMergeGeometries(True)
    sg_aggregation_pipeline_settings.SetEnableGeometryCulling(True)
    sg_aggregation_pipeline_settings.SetGeometryCullingPrecision(culling_precision)

    sg_aggregation_pipeline.RunSceneFromFile(input_file, output_file, Simplygon.EPipelineRunMode_RunInNewProcess)


def main():
    export_file_path = unreal.Paths.project_intermediate_dir() + "/Simplygon/" + "exported_scene.sgscene"
    processed_file_path = unreal.Paths.project_intermediate_dir() + "/Simplygon/" + "processed_scene.sgscene"

    with unreal.ScopedSlowTask(3, "Export from Unreal") as slow_task:
        slow_task.make_dialog(True)

        slow_task.enter_progress_frame(1, "Export from Unreal")
        export_from_unreal(export_file_path)

        sg = simplygon_loader.init_simplygon()
        slow_task.enter_progress_frame(1, "Perform aggregation")
        aggregate_scene(sg, export_file_path, processed_file_path, 0.5)

        slow_task.enter_progress_frame(1, "Import into Unreal")
        import_to_world(processed_file_path)


if __name__== "__main__":
    main()
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*