Duplicator

In Array Modifier Grid Tutorial, we saw how we can create object in Grid.

You can modify that script to create a duplicator. The change that you have to make is, Instead of creating new object and adding array modifier, duplicate selected object and add modifier on duplicated object.

That script has limitation:

  1. If you want to use same mesh data. (In viewport, alt D instead of shift D)

  2. If object has animation or material that you want to handle separately based on situation.

So, we need a way to duplicate/create objects.

Note

I had written this script as blender addon for an “Animation Nodes” project. If you find it’s settings unintuitive that’s because it is very project oriented. The blender operator version of this script is much shorter. Reason being, changing parameters(property) in operators, undos old operation and redos operation with new parameters. While I can delete and recreate in code too, but I have used simple ‘Object Pool’ 1 mechanism.

Note

This script is a bit advanced. This is over 300 lines long, but that is because I have copied some very common functions related to collection and object creation from a package that I keep in module folder. In my PC I could have imported that module. Last 100 lines or so is code related to UI creation. ‘Object Pool’ functionality has taken a lot of lines too.

Most important function is at line 98 onCopiesChanged you can go through that and read comments.

last line in ui_elements is op.onCancel.append(cleanup) which appends cleanup function to delete any remaining extra objects when operator finishes.

This can have bugs.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# "name": "Duplicates selected objects in Grid"
# bl_label = "My AN Duplicate Objects in Grid"

from boss.ui_creator import UICreator,Boss_OT_base_ui,RectData,FieldValue
import bpy
from mathutils import Vector, Euler, Matrix
import math


total_copies = 0
deleted = 0

dict_objsToDelete:dict = {}
dict_createdObj:dict = {}
dict_collections:dict = {}


vbf_center                   :None
cb_inNewCollection           :None
cb_collectionUnderScene      :None
vif_copies                   :None
cb_distribution              :None
vff_translate                :None


def find_collection(context, item):
    collections = item.users_collection
    if len(collections) > 0:
        return collections[0]
    return context.scene.collection

def make_collection(collection_name, parent_collection):
    if collection_name in bpy.data.collections:  # Does the collection already exist?
        return bpy.data.collections[collection_name]
    else:
        new_collection = bpy.data.collections.new(collection_name)
        parent_collection.children.link(new_collection)  # Add the new collection under a parent
        return new_collection


def delete_collection(collection_name):
    allColls = bpy.data.collections
    allColls.remove(bpy.data.collections[collection_name], do_unlink=True)


def getCollection(obj):
    inNewCollection, collectionUnderScene = (cb_inNewCollection.value, cb_collectionUnderScene.value)
    C = bpy.context
    objCollection = find_collection(C, obj)

    # new_collection is the collection where duplicated objects will be
    # childed.

    new_collection = objCollection

    if inNewCollection:
        if collectionUnderScene:
            new_collection = make_collection(obj.name + '_duplicated', C.scene.collection)
        else:
            new_collection = make_collection(obj.name + '_duplicated', objCollection)

    return new_collection

def copyObj(objToCopy, copyMesh, copyMat, copyAnim):
    ob = objToCopy.copy()
    if copyMesh:
        ob.data = ob.data.copy()
    if copyMat:
        for slot in ob.material_slots:
            slot.material = slot.material.copy()
    if copyAnim:
        if objToCopy.animation_data and objToCopy.animation_data.action:
            ob.animation_data.action = objToCopy.animation_data.action.copy()
        else:
            # print('object has no animation data')
            pass
    return ob

def cleanup():
    print('cleanup')
    allObjs = bpy.data.objects

    renameObjs()
    for objList in dict_objsToDelete.values():
        for o in objList:
            allObjs.remove(o, do_unlink=True)

def renameObjs():
    copies = vif_copies.value
    for obj, objList in dict_createdObj.items():
        i = 0
        for x in range(copies[0]):
            for y in range(copies[1]):
                for z in range(copies[2]):
                    objList[i].name = obj.name + '_dupli_' + str(x) + '_' + str(y) + '_' + str(z)
                    i += 1

def onCopiesChanged():
    global total_copies
    global deleted
    global dict_objsToDelete

    copies = vif_copies.value
    new_total_copies = copies[0] * copies[1] * copies[2]

    if new_total_copies > total_copies:
        # new objects will be created.
        objToCreateCnt = new_total_copies - total_copies

        # check if objects are available in deleted
        if deleted >= objToCreateCnt:
            # enough objects are in deleted dict.
            # new objects won't be created.
            for (obj, deletedObjList), createdObjList in zip(dict_objsToDelete.items(), dict_createdObj.values()):
                moveToCreated = deletedObjList[:objToCreateCnt]
                del deletedObjList[:objToCreateCnt]
                for mtc in moveToCreated:
                    mtc.hide_set(False)
                    createdObjList.append(mtc)

            deleted = deleted - objToCreateCnt
        else:
            # not enough objects in deleted
            # first move all deleted to the created.
            for (obj, deletedObjList), createdObjList in zip(dict_objsToDelete.items(), dict_createdObj.values()):
                moveToCreated = deletedObjList[:]
                deletedObjList.clear()
                for mtc in moveToCreated:
                    mtc.hide_set(False)
                    createdObjList.append(mtc)

            toCreateMore = objToCreateCnt - deleted

            for (obj, coll), createdObjList in zip(dict_collections.items(), dict_createdObj.values()):
                for i in range(toCreateMore):
                    ob = copyObj(obj, False, False, True)
                    ob.name = obj.name + '_dupli_' + str(total_copies + deleted + i)
                    coll.objects.link(ob)
                    createdObjList.append(ob)

            deleted = 0
    else:
        # objects will be destroyed.
        # number of objects to delete from each object list
        objToDelCnt = total_copies - new_total_copies
        for obj, createdObjList in dict_createdObj.items():
            temp = createdObjList[-objToDelCnt:]

            del createdObjList[-objToDelCnt:]

            for t in reversed(temp):
                t.hide_set(True)
                dict_objsToDelete[obj].insert(0,t)


        deleted += objToDelCnt

    total_copies = new_total_copies

    placeObjects()

def placeObjects():
    center, distribution, translate, copies = (vbf_center.value, cb_distribution.value, vff_translate.value, vif_copies.value)

    global dict_createdObj
    global dict_collections

    for obj, objList in dict_createdObj.items():
        initLoc = Vector((0, 0, 0))
        step = Vector((0, 0, 0))  # gap between objects

        if distribution:
            step = translate
            initLoc[0] = obj.location[0] - ((copies[0] - 1) * step[0]) / 2 if center[0] else obj.location[0]
            initLoc[1] = obj.location[1] - ((copies[1] - 1) * step[1]) / 2 if center[1] else obj.location[1]
            initLoc[2] = obj.location[2] - ((copies[2] - 1) * step[2]) / 2 if center[2] else obj.location[2]

        else:
            step[0] = 0 if copies[0] == 1 else translate[0] / (copies[0] - 1)
            step[1] = 0 if copies[1] == 1 else translate[1] / (copies[1] - 1)
            step[2] = 0 if copies[2] == 1 else translate[2] / (copies[2] - 1)

            initLoc[0] = obj.location[0] if copies[0] == 1 else (obj.location[0] - translate[0] / 2 if center[0] else obj.location[0])
            initLoc[1] = obj.location[1] if copies[1] == 1 else (obj.location[1] - translate[1] / 2 if center[1] else obj.location[1])
            initLoc[2] = obj.location[2] if copies[2] == 1 else (obj.location[2] - translate[2] / 2 if center[2] else obj.location[2])


        i = 0
        for x in range(copies[0]):
            for y in range(copies[1]):
                for z in range(copies[2]):
                    # transformations:
                    objList[i].location[0] = initLoc[0] + step[0] * x
                    objList[i].location[1] = initLoc[1] + step[1] * y
                    objList[i].location[2] = initLoc[2] + step[2] * z
                    i += 1

def onUnderSceneCollectionPressed():
    if not cb_inNewCollection.value:
        return
    else:
        if cb_collectionUnderScene.value:
            for obj, coll in dict_collections.items():
                objColl = find_collection(bpy.context, obj)
                objColl.children.unlink(coll)
                bpy.context.scene.collection.children.link(coll)
        else:
            for obj, coll in dict_collections.items():
                objColl = find_collection(bpy.context, obj)
                bpy.context.scene.collection.children.unlink(coll)
                objColl.children.link(coll)

def onNewCollectionChanged():
    global dict_collections
    global dict_createdObj

    for obj, objList in dict_createdObj.items():
        oldColl = dict_collections[obj]
        newColl = getCollection(obj)

        if newColl == oldColl:
            return
        else:
            dict_collections[obj] = newColl
            for o in objList:
                oldColl.objects.unlink(o)
                newColl.objects.link(o)
            if cb_inNewCollection.value:
                pass
            else:
                delete_collection(obj.name + '_duplicated')

def ui_elements_inGrid(op: Boss_OT_base_ui):
    """ duplicated selected objects in order in new collection """
    ttt_under_scene = (
    'If collection is not on, then ignored',
    'otherwise created collection will be under',
    "'Scene collection' if on, and under object's",
    'collection if off '
    )

    objs = bpy.context.selected_objects

    if len(objs) < 1:
        return {'no object selected, select something'}

    global vbf_center
    global cb_inNewCollection
    global cb_collectionUnderScene
    global vif_copies
    global cb_distribution
    global vff_translate

    global total_copies
    global dict_createdObj
    global dict_collections

    cc = UICreator
    mouse_x,mouse_y = cc.mouse_xy(op)

    btn_title = cc.button(op, rectData=(mouse_x,mouse_y-50,200,50),text='DuplicateGrid')
    
    fv = FieldValue(
        value=(3, 3, 1),
        min=(0, 0, 0),
        max=(10, 10, 10),
        changeBy=(1, 1, 1)
    )
    
    vif_copies = cc.vectorIntField(op, btn_title.rectData.getBottom(5), fv, onCopiesChanged,
                                   parent=btn_title, canDrag=False)

    vff_translate = cc.vectorFloatField(op, vif_copies.rectData.getBottom(5), (2.0, 2.0, 2.0), placeObjects, parent=btn_title,canDrag=False)

    cb_distribution = cc.checkBox(op, vff_translate.rectData.getBottom(5), 'distribution', True, placeObjects, parent=btn_title,canDrag=False)  # True is step , false is range

    vbf_center = cc.vectorBooleanField(op, cb_distribution.button.rectData.getBottom(5), value=(True, True, True),
                                              onValueChange=placeObjects, parent=btn_title,canDrag=False)

    cb_inNewCollection = cc.checkBox(op, rectData=vbf_center.rectData.getBottom(5), text='new collection',
                                            ttt='Create objects in new collection',
                                            value=True, onValueChange=onNewCollectionChanged,parent=btn_title,canDrag=False)


    cb_collectionUnderScene = cc.checkBox(op, rectData=cb_inNewCollection.button.rectData.getBottom(5),
                                                 text='under the scene', value=False, ttt=ttt_under_scene,
                                                 onValueChange=onUnderSceneCollectionPressed,parent=btn_title,canDrag=False)


    

    total_copies = vif_copies.value[0] * vif_copies.value[1] * vif_copies.value[2]

    # create collections in the beginning

    dict_collections = {obj : getCollection(obj) for obj in objs}

    dict_createdObj.update({o: [] for o in objs})
    dict_objsToDelete.update({o: [] for o in objs})

    # Create new objects and link those to collection
    for (obj, coll), createdObjList in zip(dict_collections.items(), dict_createdObj.values()):
        for i in range(total_copies):
            ob = copyObj(obj, False, False, True)
            ob.name = obj.name + '_dupli_' + str(i)
            coll.objects.link(ob)
            createdObjList.append(ob)

    # place objects
    placeObjects()

    op.onCancel.append(cleanup)


Footnotes:

1

Object Pool is a technique, frequently used in games, in which, objects are created/destroyed from a pool of objects. So, a few objects can be created in the beginning and whenever objects need to be created/deleted they are pulled from/pushed to that pool. This increases the performance as memory is not freed every time an object is deleted and then again allocated when object is recreated. So, it’s easy on garbage collector.