Object_instances changes in 3.0

I’m trying to update our render engine addon to deal with the new change to object instances in 3.0, specifically the API changes (Reference/Release Notes/3.0/Python API - Blender Developer Wiki) mentioned:

Previously, .object.data and .instance_object.data were always the same. Now they can be different and it is correct to use .object.data. object_instance.object: A temporary object wrapping the instance. References to this object must not be stored between loop iterations.

Our addon used to map bpy.types.Object to our own data classes in a dictionary, something like this:

my_objs = dict()
for ob in [x for x in depsgraph.ids if isinstance(x, bpy.types.Object)]:
    my_obj = export(ob)
    my_objs[ob.original] = my_obj
    # do something my_obj

however, with this new change this doesn’t seem to be the recommended any more. It seems the recommended way would be to use the object.data as the key. Something like:

my_objs = dict()
for ob_inst in depsgraph.object_instances:
    ob = ob_inst.object
    if ob.data.original in my_objs:
        my_obj = my_objs.get(ob.data.original)
        my_objs[ob.data.original] = my_obj
    else:
        my_obj = export(ob)
   # do something with my_obj

This works in that my_obj gets re-used when the same object.data is used. However, this doesn’t seem to work when looping through despgraph.updates during a viewport render (ex: the user is editing the mesh). We can’t seem to use the DepsgraphUpdate.id.original as the key to the dictionary, as it says that key doesn’t exist.

Are we going about this the wrong way? I’d rather not use the .name as the key as I really don’t want to have to deal with renaming.

I just tried some simple tests with the default cube. First I save the object data ID:

c1 = id(bpy.data.objects["Cube"].data)

Then I do some editing of the mesh, move a vertex, delete a vertex, then check the data ID again:

c1 == id(bpy.data.objects["Cube"].data)

and it returns True.

Are you doing something different from this?

I think the difference is that you’e not comparing with what’s coming from depsgraph. Does this work for you?

import bpy

depsgraph = bpy.context.evaluated_depsgraph_get()
objs = dict()
cube = bpy.data.objects['Cube'].data.original
objs[cube] = True

print("Find cube in despgraph.object_instances")
for ob_inst in depsgraph.object_instances:
    test = ob_inst.object.data.original
    if test in objs:
        print("\tFound cube")
print("Finished search")

i.e.: does “Found cube” get printed?

I thought these should be the same. It also seems object.data of instances of the Cube on a different object (ex: a curve that uses the Cube with the “instances on points” geometry node) are different than instances of the Cube itself.

I wrote a simple render engine addon to test this.

import bpy
import bgl
import blf
import numpy as np

bl_info = {
    "name": "Test Render Engnie",
    "author": "ihsieh",
    "version": (1, 0, 0),
    "blender": (3, 0, 0),
    "location": "Info Header, render engine menu",
    "description": "My Test RE",
    "warning": "",
    "category": "Render"}

class MyMesh:
    def __init__(self, name):
        self.name = name
        self.points = []

    def update(self, mesh):
        nvertices = len(mesh.vertices)
        P = np.zeros(nvertices*3, dtype=np.float32)
        mesh.vertices.foreach_get('co', P)
        P = np.reshape(P, (nvertices, 3))
        self.points = P.tolist()

class TestRenderEngine(bpy.types.RenderEngine):
    bl_idname = 'TEST_RENDER'
    bl_label = "TestRenderEngine"
    bl_use_preview = False 
    bl_use_save_buffers = True
    bl_use_shading_nodes = True 
    bl_use_eevee_viewport = True 
    bl_use_postprocess = True

    def __init__(self):
        self.scene_data = None

    def __del__(self):
        pass

    def render(self, depsgraph):        
        pass

    def view_update(self, context, depsgraph):
        region = context.region
        view3d = context.space_data
        scene = depsgraph.scene
        objects_updated = list()

        if not self.scene_data:
            # First time initialization
            self.scene_data = dict()

            # Loop over all datablocks used in the scene.
            for datablock in depsgraph.ids:
                if not isinstance(datablock, bpy.types.Mesh):
                    continue
                my_mesh = MyMesh(datablock.name)
                self.scene_data[datablock.original] = my_mesh

            # Loop over all instances
            for instance in depsgraph.object_instances:
                ob = instance.object
                if ob.type != 'MESH':
                    continue
                datablock = ob.data
                if datablock.original not in self.scene_data:
                    # this datablock didn't get exported in the loop above
                    my_mesh = MyMesh(datablock.name)
                    self.scene_data[datablock.original] = my_mesh                    
                else:
                    my_mesh = self.scene_data[datablock.original]
                my_mesh.update(datablock)            

        else:
            objects_updated = list()

            # Test which datablocks changed
            for update in depsgraph.updates:
                if isinstance(update.id, bpy.types.Object):
                    print("Datablock updated: %s" % update.id.name)
                    objects_updated.append(update.id.original)

            # Loop over all object instances, see which instances changed.
            if depsgraph.id_type_updated('OBJECT'):
                for instance in depsgraph.object_instances:
                    ob = instance.object
                    if ob.type != 'MESH':
                        continue
                    if instance.is_instance:
                        if instance.instance_object.original not in objects_updated:
                            print("\tInstance object not updated: %s" % ob.name)
                            continue
                    elif ob.original not in objects_updated:
                        print("\tObject not updated: %s" % ob.name)
                        continue
                    datablock = ob.data
                    if datablock.original not in self.scene_data: 
                        print("\tNot in objects_updated: %s" % datablock.name)
                        continue
                    my_mesh = self.scene_data[datablock.original]
                    print("\tUpdating datablock: %s" % datablock.name)
                    my_mesh.update(datablock)
                    
    def view_draw(self, context, depsgraph):
        pass


classes = [
    TestRenderEngine,
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
def unregister():
    
    for cls in classes:
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass

Using the scene file attached (the forum doesn’t to allow for uploads of .blend file, so I name it .txt), how do I know if the cubes that the BezierCircle has instanced have been updated? Ex: open the scene, start a viewport render, select Cube and press Tab to go to edit mode. The depsgraph.updates loop let’s me know that BezierCurve object has updated, but I need to reference its data from our scene_data dictionary.

geonodes_instancing_test_re.txt (811.0 KB)