11

Is there a better way to copy a modifier with all attributes from active object to selected object through python scripting other than adding a modifier to selected and copy attributes one by one (with dictionary for example) from active?

The problem is some attributes are read-only (even if it makes no sense - like for example subdivision algorithm in Subsurf modifier) and i don't know how to set them through python script. So i can't copy that subdivision algorithm value from one to another through scripting with method setting a value for an attribute:

bpy.data.objects[object_name].modifiers[modifier_name].attribute_name = attribute_value

So to reiterate my question: is there any other way to add a modifier to an object that would allow setting up values for all attributes?

kilbee
  • 1,327
  • 12
  • 22
  • You could copy the object (incl. modifiers) and let a script set all non-modifier related properties to the target object's properties, but it's basically the same as copying modifier attributes... – CodeManX Nov 15 '13 at 01:38
  • You may want to investigate the source of the Copy Attributes add-on. It provides the functionality of copying modifiers. At the very least, you can see how it's done there. – Fweeb Nov 15 '13 at 18:38

5 Answers5

19

I don't think that's possible.

A modifier can only be inserted through modifiers.new, so its properties do need to be copied individually. The problem here is in checking which properties can be copied. (e.g., Subsurf modifier's subdivision_type property is actually not readonly as you claimed, but we'd need to test it manually or check the API reference to find out.)

The whole "check writability then copy" procedure can be done more efficiently, though. For example, see this function:

import bpy

def main():
    active_object = bpy.context.object
    selected_objects = [o for o in bpy.context.selected_objects
                        if o != active_object and o.type == active_object.type]

    for obj in selected_objects:
        for mSrc in active_object.modifiers:
            mDst = obj.modifiers.get(mSrc.name, None)
            if not mDst:
                mDst = obj.modifiers.new(mSrc.name, mSrc.type)

            # collect names of writable properties
            properties = [p.identifier for p in mSrc.bl_rna.properties
                          if not p.is_readonly]

            # copy those properties
            for prop in properties:
                setattr(mDst, prop, getattr(mSrc, prop))

main()

It copies all modifiers from active object, to every other selected objects of similar type. What modifier properties to copy is determined by checking is_readonly attribute of all items in the modifier's bl_rna.properties. This way, we can ensure that only writable properties get copied.

In the API Reference, some readonly attributes like Dynamic Paint's brush_settings have no other method of access, so they can be set only through the UI.

Adhi
  • 14,310
  • 1
  • 55
  • 62
  • Thanks for clarifying. For some reason i still can't write some attributes with my code (i've simply used try/except to avoid read-only attributes, so there must be other reason that prevented some of the attributes from updating), but your code works without a problem. Thank you. – kilbee Nov 15 '13 at 12:24
  • Truly awesome answer! – Antoni4040 Jun 10 '14 at 12:01
  • great answer!!!! – Chris Aug 28 '21 at 16:34
6

A simpler method than Adhi's is to use make_links_data

Say you make two cubes. You apply modifiers to the first one and want to copy them to the second one. Then your code should look something like this:

import bpy

mod_obj = bpy.context.scene.objects["Cube"]           # cube with mods
modable_obj = bpy.context.scene.objects["Cube.001"]   # cube without mods

mod_obj.select = True
bpy.context.scene.objects.active = mod_obj
modable_obj.select = True

bpy.ops.object.make_links_data(type='MODIFIERS')

bpy.ops.object.select_all(action='DESELECT')
neowitch
  • 101
  • 2
  • 5
1

I attempted to refactor Adhi's answer to a class for easier usage:

from typing import Any, NamedTuple

import bpy

class ModifierSpec(NamedTuple): name: str type: str properties: dict[str, Any]

@classmethod
def from_modifier(cls, modifier):
    spec = cls(modifier.name, modifier.type, {})

    for prop in modifier.bl_rna.properties:
        if prop.is_readonly:
            continue

        name = prop.identifier
        spec.properties[name] = getattr(modifier, name)

    return spec

def add_to_object(self, obj):
    new_modifier = obj.modifiers.get(self.name, None)
    if new_modifier is None:
        new_modifier = obj.modifiers.new(self.name, self.type)

    for name, prop in self.properties.items():
        setattr(new_modifier, name, prop)


collect modifiers from object

modifier_specs: list[ModifierSpec] = [] for old_modifier in source_obj.modifiers: modifier_specs.append(ModifierSpec.from_modifier(old_modifier))

add modifiers from spec

for spec in modifier_specs: spec.add_to_object(dest_obj)

James
  • 11
  • 2
  • Looks nice and tidy. You might want to add a guard clause somewhere if the user tries to copy modifiers to an object that doesn't support it like an empty object, since new_modifier will then be None and the subsequent lines will throw an error. Thank you for sharing ! – Gorgious May 11 '23 at 07:03
1

Based on Adhi's answer.

Maybe below can be useful for someone in the future. Can be more Pythonic and beautiful, but I think below should collect all the Modifier Props from ob for obtarget, providing obtarget already has all the Modifiers being generated based on ob.

import bpy

ob = bpy.context.active_object
obtarget = bpy.data.objects['Cube.001']

mod_props = []

for mod in ob.modifiers:
    properties = []
    for prop in mod.bl_rna.properties:
        if not prop.is_readonly:
            properties.append(prop.identifier)
    mod_props.append([mod, properties])

for stuff in mod_props:
    for prop in stuff[1]:
        setattr(obtarget.modifiers[stuff[0].name], prop, getattr(stuff[0], prop))
Blender Sushi Guy
  • 1,491
  • 11
  • 18
0

Another solution which uses an operator override of object.modifier_copy_to_selected and doesn't require you to select objects.

import bpy

source = bpy.data.objects["Cube"] target = bpy.data.objects["Cube.001"] modifier_name = "Solidify"

with bpy.context.temp_override(object=source, selected_objects=(source, target)): bpy.ops.object.modifier_copy_to_selected(modifier=modifier_name)

Gorgious
  • 30,723
  • 2
  • 44
  • 101