How to replicate functionality of Merge Actors / Batch on a Python script

How can I call on Merge Actors / Batch (https://dev.epicgames.com/documentation/en\-us/unreal\-engine/merging\-actors\-in\-unreal\-engine\#batch) from a python script?

I can see merge_static_mesh_actors(actors_to_merge, merge_options) on the StaticMeshEditorSubsystem, but there is no option similar to Batch I can pass on unreal.MergeStaticMeshActorsOptions

I also found unreal.MeshInstancingSettings which makes me think there should be a method I can call somewhere that takes this as an argument, but I just haven’t been able to find it on the Python API page.

Hello there,

It appears that Merge Actors directly calls FMeshMergeUtilities::MergeComponentsToInstances to perform instancing. This same function takes MeshInstancingSettings, but the functions, and the class it resides in, are not exposed to blueprint.

I believe FMeshInstancingSettings has the Blueprintable specifier on it to allow its use in UMeshInstancingSettingsObject rather than to enable this functionality from BP or Python.

Converting to ISM is fortunately much easier than performing mesh merge or approximation. The general outline of the script would want to get the select objects, group them by static mesh, then create ISM actors for each group with the instance transforms of any given ISM actor set to the actor transforms of the each member of said group.

I hope that helps.

Best regards,

Chris

Hi Chris,

Thank you for your response. Indeed it seems a simple operation, and I did write a script to do it:

def batch_convert_to_instanced_static_mesh():
    """
    Batch convert selected static mesh actors to instanced static mesh components.
    Groups actors by their static mesh and creates one ISMC actor per unique mesh.
    """
    
    editor_util_lib = unreal.EditorUtilityLibrary()
    editor_actor_subsystem = unreal.EditorActorSubsystem()
    
    # Get selected actors
    selected_actors = editor_actor_subsystem.get_selected_level_actors()
    
    if len(selected_actors) < 2:
        print("Please select at least 2 static mesh actors to batch convert")
        return False
    
    # Filter to only static mesh actors and group by mesh
    mesh_groups = defaultdict(list)
    
    for actor in selected_actors:
        if isinstance(actor, unreal.StaticMeshActor):
            # Get the static mesh component
            static_mesh_comp = actor.get_component_by_class(unreal.StaticMeshComponent)
            if static_mesh_comp:
                mesh_asset = static_mesh_comp.get_editor_property("static_mesh")
                mesh_groups[mesh_asset].append(actor)
    
    if not mesh_groups:
        print("No valid static mesh actors found in selection")
        return False
    
    print(f"Found {len(mesh_groups)} unique meshes to convert to instanced components")
    
    created_actors = []
    
    # Process each group of actors with the same mesh
    for mesh_asset, actors in mesh_groups.items():
        if len(actors) < 2:
            print(f"Skipping {mesh_asset.get_name()} - only {len(actors)} instance(s)")
            continue
            
        print(f"Converting {len(actors)} instances of {mesh_asset.get_name()}")
        
        # Create new actor for the instanced static mesh
        new_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.Actor, unreal.Vector(0, 0, 0))
        new_actor.set_actor_label(f"ISMC_{mesh_asset.get_name()}")
 
        # Add InstancedStaticMeshComponent to the new actor
        sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)
        handles = sds.k2_gather_subobject_data_for_instance(new_actor)
        if not handles:
            raise RuntimeError("No subobject handles found for actor")
 
        root_handle = handles[0]  # pick the actor root handle
        params = unreal.AddNewSubobjectParams(
            parent_handle=root_handle,
            new_class=unreal.InstancedStaticMeshComponent
        )
 
        result = sds.add_new_subobject(params)
        if isinstance(result, tuple):
            success, new_handle = result if isinstance(result[0], bool) else (result[1], result[0])
        else:
            success, new_handle = True, result
 
        if not success:
            raise RuntimeError("add_new_subobject failed to create the component")
        
        # Set static_mesh property
        subobj_data = sds.k2_find_subobject_data_from_handle(new_handle)
        new_static_mesh_comp = unreal.SubobjectDataBlueprintFunctionLibrary.get_object( subobj_data )
        if new_static_mesh_comp:
            new_static_mesh_comp.set_editor_property("static_mesh", mesh_asset)
        
        # Add instances for each original actor
        instance_transforms = []
        for actor in actors:
            transform = actor.get_actor_transform()
            instance_transforms.append(transform)
        
        # Add all instances at once (more efficient)
        new_static_mesh_comp.add_instances(instance_transforms, True, True)
        
        print(f"Created ISMC actor with {len(actors)} instances")
    
    print(f"Successfully created {len(created_actors)} instanced static mesh actors")
    
    # Select the newly created actors
    if created_actors:
        editor_util_lib.set_selected_actor_references([unreal.EditorActorSubsystemReference(actor) for actor in created_actors])
    
    return True

However, when running this script on certain Datasmith files I’m working with, I’ve noticed that some geometry ends up instanced with flipped normals. Interestingly, the same file instances correctly when using Merge Actors, which makes me think it’s doing some form of normal evaluation and correction under the hood.

That’s mainly why I was hoping to call the native functionality directly rather than recreating it. Since that’s not possible, I’ll look into incorporating normal checks and corrections into my workflow.

Thanks again!

Nahuel

Do any of the static mesh actors have a negative scale?

I remember ISMs not supporting negatively scaled instances due to face culling and the solution being to create a second ISM for the negatively scaled instances with the culling reversed.

I think this is how PackedLevelActors handles negatively scaled actors.

Best regards,

Chris