przemir/ApplyModifierForObjectWithShapeKeys

Script process optimization suggestions

Andrej730 opened this issue ยท 6 comments

Couple suggestions:

  1. If object has n shape keys then the script will create n temporary objects and keep them at the scene at the same time which could be very memory consuming.
    I was using 60MB model with 160 shape keys and it immediately preserved areound 10GB of RAM and script almost froze.
    Solution is to create only 1 temporary object at the time, add it to resulting object as shape key and remove.

  2. Replace operator (bpy.ops) commands with blender python api analogues where it's possible, it would save us some time.
    Removing 159 shape keys for one temporary object took for me around around 120 seconds, which makes it 159 x 120 / 3600 = 5.3 hours to apply the modifier with the shape keys, so any way to reduce this time will be nice.
    Maybe solving the probem I've first mentioned above would reduce this time drastically and there will be no need to replace operator commands.

  3. Currently if you need to apply multiple modifiers with the script you just use it couple times which once again could be very time consuming working with large amount of shape keys.
    Maybe adding option to apply all modifiers at once would help the process? (not sure about this one because it could make script more resource consuming which will actually slow the process)

ad1. I think I found even better solution thanks to "Transfer shape key" and "Delete all shape keys" actions:

  1. Original object is copied, then all shape keys are removed from copy.
  2. The rest of objects are copied from object created during previous step (they are without shape keys so they should be lighter)
  3. For each shape key except base transfer one from original object to corresponding copy. Then remove base shape for copies and apply modifiers.
  4. For original object, remove all shape keys, then apply modifiers.
  5. Joining shape keys (nothing new here).

ad2. Replacing bpy.ops commands requires I know how they exactly behave to ensure I don't forget about some detail. The algorithm changed so there should be less 'bpy.ops.object.shape_key_remove()' calls. Copying objects without shape keys should be much faster. Hovewer first object still will be copied with all shape keys.

ad3. Some modifiers (like armature) shouldn't be applied. Sometimes we want to apply only some of modifiers. So user should choose which modifers to apply. I changed combobox with list with checkboxes. It is possible now to check more than one modifier.

I uploaded changes.

I've tried new version of the script and in my case it crashes Blender (my case: 60 MB model with 160 shape keys, applying level 1 subsurface and I have not so good PC setup - 8 GB RAM, 4 threads CPU) - I think it happens because it creates 160 copies of the object and it's really resource heavy. For my friend it completely loads his 16 GB RAM and 16 CPU threads, we waited around 5-10 minutes and script was still loading.

From my experience removing shape keys will make the object lighter but if shape key doesn't change much of the mesh then removing it won't make object much lighter.

So I still think the preferable method of optimization will be to create only 1 copy of the object at the time.
I've made my own version of the script to apply this principle and it takes around 3-4 minutes to apply modifier to the model I've mentioned above. It doesn't seem to load much of RAM and CPU and I'm able to use it even on my PC.

Here's the main part of the mine script - hope it will be any useful:

def applyModifierForObjectWithShapeKeys():    
    base_model = bpy.context.object
    result_model = create_temp(base_model, 0, modifiers=['Subsurf'])
    result_model.name = base_model.name + " modified"

    # creating Basis shape key for resulting model
    bpy.context.view_layer.objects.active = result_model
    bpy.ops.object.shape_key_add(from_mix=False)

    base_shape_keys = list(base_model.data.shape_keys.key_blocks.keys())
    n_shape_keys = len(base_shape_keys)
    for shape_key_i in range(1, n_shape_keys):
        # creating temp model and applying it to base model as shape key[
        temp_model = create_temp(base_model, shape_key_i, modifiers=['Subsurf'])
        temp_model.name = base_model.name + " temp.000" 
        
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.objects.active = result_model
        temp_model.select_set(True)    
        
        bpy.ops.object.join_shapes()
        
        result_model.data.shape_keys.key_blocks[shape_key_i].name = base_shape_keys[shape_key_i]
        bpy.data.objects.remove(temp_model, do_unlink=True)
        print(f'Applying {shape_key_i} finished.')
    print('script is finished')
    return (True, None)


def create_temp(base_model, n_shape_key, modifiers=[]):
    # n_shape_key is the index of the shape key that will be preserved
    t0 = time()
    
    temp_model = duplicate(base_model)
    n = len(temp_model.data.shape_keys.key_blocks)
    
    print(f'working on {temp_model.name}')
    shape_keys = list(temp_model.data.shape_keys.key_blocks.keys())
    for i, shape_key in enumerate(shape_keys):
        if i == n_shape_key:
            continue
        temp_model.shape_key_remove(temp_model.data.shape_keys.key_blocks[shape_key])

    # last removed shape key is applied to object
    temp_model.shape_key_remove(temp_model.data.shape_keys.key_blocks[0])

    if modifiers:
        bpy.context.view_layer.objects.active = temp_model
        for modifier in temp_model.modifiers:
            bpy.ops.object.modifier_apply(modifier=modifier.name)
            
    print(f'creating temp took us {time()-t0}')
    return temp_model

def duplicate(obj, data=True, actions=True, collection=None):
    obj_copy = obj.copy()
    if data:
        obj_copy.data = obj_copy.data.copy()
    obj.users_collection[-1].objects.link(obj_copy)
    print(f'duplicated object name {obj_copy.name}')
    return obj_copy

My bad, I wrongly assumed shape keys holds all vertices. I changed algorithm so now there is maximum 3 copies (including original object) at time.

I wanted to preserve original object just in case copying will change/rename something inside I don't know.

I didn't use replacement for ops copying and deleting as I don't want to overlook case when there is something to do beside handling object/data block. But thanks for the code as I didn't know this another method for copying and deleting.

Just tested it, it works and I'm able to apply modifiers even on my slow pc.

My only suggestion left is to add some way to track the progress of the script - maybe just printing some message to system console: "Applying shape key 124/200. @120 sec."
(where "@120 sec" means it tooks 120 seconds to get to this part of the script)

I added logs.

Thank you!