diff --git a/kubric/core/objects.py b/kubric/core/objects.py index cd06f665..d47993e3 100644 --- a/kubric/core/objects.py +++ b/kubric/core/objects.py @@ -284,4 +284,22 @@ class FileBasedObject(PhysicalObject): # UI, minus a 90 degree X-axis rotation applied after loading. glb_do_transform_apply_after_import = tl.Bool(False) + # If true, uses a parenting approach instead of a join so that the asset + # represents a top-level Empty node in the scene. All existing objects + # without parents will be parented to this object. Any transform applied to + # this object will be applied to all children. For more details, see + # https://docs.blender.org/manual/en/latest/scene_layout/object/editing/parent.html + # + # Parenting avoids destroying things like animations and can preven + # accidental deletion of objects from the scene. + # + # This will break certain features of kubric like assigning meterials to + # assets or other functions that expect the asset to be a mesh. For + # example, when assigning segmentation_ids you may want to reconstruct + # kb.Asset recursively on the top-level empty and set the `segmentation_id`. + # Also when used with PyBullet for simulations, the non-animated collision + # mesh is used which can lead to unexpected behavior when an animated object + # is used in a physics simulation. + use_parenting_instead_of_join = tl.Bool(False) + # TODO: trigger error when changing filenames or asset-id after the fact diff --git a/kubric/renderer/blender.py b/kubric/renderer/blender.py index 0d9df4b7..b3d53f14 100644 --- a/kubric/renderer/blender.py +++ b/kubric/renderer/blender.py @@ -36,6 +36,24 @@ logger = logging.getLogger(__name__) +def add_top_level_empty_parent(name: str = "Empty") -> bpy.types.Object: + """Adds an empty parent to scene and makes it the parent of all objects. + + Args: + name: The name of the empty parent. + + Returns: + The newly created empty parent. + """ + parent_obj = bpy.data.objects.new(name, None) + parent_obj.rotation_mode = "QUATERNION" + bpy.context.scene.collection.objects.link(parent_obj) + for obj in bpy.context.scene.objects: + if obj != parent_obj and obj.parent is None: + obj.parent = parent_obj + return parent_obj + + # noinspection PyUnresolvedReferences class Blender(core.View): """ An implementation of a rendering backend in Blender/Cycles.""" @@ -422,29 +440,40 @@ def _add_asset(self, obj: core.FileBasedObject): location=True, rotation=True, scale=True ) - # gltf files often contain "Empty" objects as placeholders for camera / lights etc. - # here we are interested only in the meshes, we filter these out and join all meshes into one. - mesh = [m for m in bpy.context.selected_objects if m.type == "MESH"] - assert mesh - for ob in mesh: - ob.select_set(state=True) - bpy.context.view_layer.objects.active = ob - - # make sure one of the objects is active, otherwise join() fails. - # see https://blender.stackexchange.com/questions/132266/joining-all-meshes-in-any-context-gets-error - bpy.context.view_layer.objects.active = mesh[0] - bpy.ops.object.join() - - # Make sure to delete all remaining non-mesh objects. Note that for - # some reason deleting the non-mesh objets before joining removes - # parts of the meshes in some cases. - non_mesh_objects = [ - obj - for obj in bpy.context.selected_objects - if obj.type != "MESH" - ] - with bpy.context.temp_override(selected_objects=non_mesh_objects): - bpy.ops.object.delete() + if obj.use_parenting_instead_of_join: + parent_obj = add_top_level_empty_parent(obj.uid) + bpy.ops.object.select_all(action="DESELECT") + parent_obj.select_set(state=True) + else: + # Legacy loader which relies on JOIN. NOTE: This will destroy + # things like animations. + # gltf files often contain "Empty" objects as placeholders for + # camera / lights etc. + # here we are interested only in the meshes, we filter these out + # and join all meshes into one. + mesh = [ + m for m in bpy.context.selected_objects if m.type == "MESH" + ] + assert mesh + for ob in mesh: + ob.select_set(state=True) + bpy.context.view_layer.objects.active = ob + + # make sure one of the objects is active, otherwise join() fails. + # see https://blender.stackexchange.com/questions/132266/joining-all-meshes-in-any-context-gets-error + bpy.context.view_layer.objects.active = mesh[0] + bpy.ops.object.join() + + # Make sure to delete all remaining non-mesh objects. Note that + # for some reason deleting the non-mesh objets before joining + # removes parts of the meshes in some cases. + non_mesh_objects = [ + obj + for obj in bpy.context.selected_objects + if obj.type != "MESH" + ] + with bpy.context.temp_override(selected_objects=non_mesh_objects): + bpy.ops.object.delete() assert len(bpy.context.selected_objects) == 1 blender_obj = bpy.context.selected_objects[0] @@ -473,7 +502,8 @@ def _add_asset(self, obj: core.FileBasedObject): # deactivate auto_smooth because for some reason it lead to no smoothing at all # TODO: make smoothing configurable - blender_obj.data.use_auto_smooth = False + if hasattr(blender_obj.data, "use_auto_smooth"): + blender_obj.data.use_auto_smooth = False register_object3d_setters(obj, blender_obj) obj.observe(AttributeSetter(blender_obj, "active_material",