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