2
$\begingroup$

I am working on an addon which bakes onto multiple meshes. Everything worked fine until I wanted to implement Ambient Occlusion baking.

To do this I need to hide all objects that have nothing to do with the bake from rendering.

Here is a pseudo code of what I am doing in the execute function:

for obj in bpy.data.objects:
    obj.hide_render = True
    obj.hide_viewport = False
bpy.data.objects['high'].hide_render = False
bpy.data.objects['high'].hide_viewport = False


for obj in bpy.data.collections[context.scene.lowpoly_bake_obj].all_objects:
    bpy.data.objects['low'].hide_render = False

    # some setup work...

    bpy.data.objects['high'].select_set(True)
    bpy.context.view_layer.objects.active = bpy.data.objects['low']

    # baking from active to selected...

    bpy.data.objects['low'].hide_render = True

Since I have introduced those hide_render and hide_viewport parts, Blender randomly crashes. But even after hours of debugging I just can't find the issue.

Here is the full source code if you want to try it out: https://github.com/flolu/blender-batch-baking/tree/0dc4bcfd17d57df49514fa6ed712e99015776c80

(especially this function here: https://github.com/flolu/blender-batch-baking/blob/0dc4bcfd17d57df49514fa6ed712e99015776c80/bake.py#L16)

Update

As suggested by Robert Gützkow, I've now tried to build Blender from source to get a stack trace when it crashes:

call stack on crash

Maybe this helps to find out what the actual problem is?

Update 2

Here is an example file, which reliably crashes when you click the "Bake" button:

crash video

The addon can be installed from here: https://github.com/flolu/blender-batch-baking/tree/0dc4bcfd17d57df49514fa6ed712e99015776c80

$\endgroup$
1
2
+50
$\begingroup$

Changing the visibility of an object (hide_render, hide_select, hide_viewport, etc.) triggers the rebuild of collection caches. Since this modifies the collection you're iterating over (Collection.all_objects), it may result in a crash. Since Blender is implemented in C and C++ you're causing undefined behavior through the Python API. Therefore it's just a (un-)lucky coincidence that it didn't crash on Windows.

This limitation is documented in Blender's Python API manual on the very important Gotcha page in the section Unforunate Corner Cases.

Unfortunate Corner Cases

Besides all expected cases listed above, there are a few others that should not be an issue but, due to internal implementation details, currently are:

  • Object.hide_viewport, Object.hide_select and Object.hide_render: Setting any of those booleans will trigger a rebuild of Collection caches, hence breaking any current iteration over Collection.all_objects.

The best solution is to create a list of all objects' names from the object references that are given by Collection.all_objects and then use these unique names to access the actual objects in the loop. This is the generally recommended approach that avoids many of the potential problems described in "Help! My script is crashing".

TL;DR: Do not keep direct references to Blender data (of any kind) when modifying the container of that data, and/or when some undo/redo may happen (e.g. during modal operators execution…). Instead, use indices (or other data always stored by value in Python, like string keys…), that allow you to get access to the desired data.

While you can create copies of the object references, there is no guarantee that these references remain valid. This could only be done when knowing exactly how Blender manages the underlying data structures. Since the internal implementation may change and doesn't have to be consistent across all versions of Blender, that approach would be considered a bad practice.

The following code shows the required modifications to avoid the crash.

Before

low_objects = bpy.data.collections[context.scene.lowpoly_bake_obj].all_objects
for obj in low_objects:
    self.bootstrap_bake(obj.name)

After

low_objects_names = [obj.name for obj in bpy.data.collections[context.scene.lowpoly_bake_obj].all_objects]
for obj_name in low_objects_names:
    self.bootstrap_bake(obj_name)

In general you would have to perform a lookup of the objects by name instead of using it directly from the collection. If you join, rename or delete objects in the loop you may also want to check that the object still exists before accessing it.

import bpy


obj_names = [obj.name for obj in bpy.data.objects]
for obj_name in obj_names:
    obj = bpy.data.objects.get(obj_name)
    if obj is not None:
        # Perform your operations with the object here

This issue was previously reported on the bug tracker in T62406.

$\endgroup$
6
  • $\begingroup$ Is it also possible to copy bpy.data.objects ? $\endgroup$ – Florian Ludewig Mar 20 '20 at 12:07
  • $\begingroup$ @FlorianLudewig as in creating a temporary copy of the referenced objects or actually copying/duplicating the objects? Both are technically possible. However, usually you should just access bpy.data.objects again. Why do you want to create a copy? Posting this as separate question might be a good idea. $\endgroup$ – Robert Gützkow Mar 20 '20 at 12:20
  • $\begingroup$ Just like the solution to the problem was to create a copy of Collection.all_objects I want to create a copy of all Objects $\endgroup$ – Florian Ludewig Mar 20 '20 at 12:23
  • $\begingroup$ @FlorianLudewig I'm updating this answer to avoid unsafe usage of the API. Copying is possible and may be used in specific circumstance, but you can easily create problematic situations as described in Help! My script crashes. The proper solution for all cases, is to create a list of the unique object names and then use these to access the actual object. $\endgroup$ – Robert Gützkow Mar 20 '20 at 13:51
  • 1
    $\begingroup$ @FlorianLudewig The answer has been updated. Using the (new) described approach is safe in all circumstances. While copying is technically possible it's not recommended for most situations. I hope that the linked and quoted sections from the manual explain this in enough detail. $\endgroup$ – Robert Gützkow Mar 20 '20 at 14:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.