Source code for ddg.visualization.blender.object

"""Collection of utility functions dealing with Blender objects
"""

import bpy
import mathutils
import numpy as np

# for clear functions
import ddg.visualization.blender.camera
import ddg.visualization.blender.collection
import ddg.visualization.blender.light
import ddg.visualization.blender.material
import ddg.visualization.blender.scene
from ddg import nonexact
from ddg.math.euclidean import normalize


[docs]def blender_object(name, data): """Wrap a Blender data-block in a Blender object. Parameters ---------- name : str (default=`None`) The `name` attribute of the object. If `None`, then use the `name` attribute of `data`. data : bpy.types.ID A Blender data-block, e.g. curves and meshes whose types - `bpy.types.Mesh` and `bpy.types.Curve` respectively - are subclasses of `bpy.types.ID`. Returns ------- bpy.types.Object """ bobj_name = data.name if name is None else name if bobj_name in bpy.data.objects: raise ValueError( f"bpy.data.objects already contains an object whose name is {bobj_name}" ) return bpy.data.objects.new(bobj_name, data)
[docs]def from_data(data, name, parent=None, matrix=np.eye(4), collection=None): """ Create an object from a data block. Parameters ---------- data : bpy.data Data to be wrapped in the object. name : str Name of the newly created object. parent : bpy.types.Object (default=None) Parent of the newly created object. matrix : numpy.ndarray (default=np.eye(4)) Local transformation of the object. collection : bpy.types.Collection (default=None) Collection to link the object to, if `None`, object will not be linked. Returns ------- bpy.types.Object Object that was created with given data. """ bobj = bpy.data.objects.new(name, data) bobj.parent = parent bobj.matrix_local = matrix bobj.location = matrix[:3, 3] if collection: collection.objects.link(bobj) return bobj
[docs]def delete(bobj): """ Delete a Blender object. Parameters ---------- bobj : bpy.types.Object Blender object to be deleted. """ bpy.ops.object.select_all(action="DESELECT") bobj.select_set(True) bpy.ops.object.delete()
[docs]def copy(bobj, collection=None): """ Makes a Copy of a Blender object and links it. Parameters ---------- bobj : bpy.types.Object Blender object to be duplicated. collection : bpy.types.Collection (default=None) Links copy to the given collection, if None, object will not be linked. Returns ------- bpy.types.Curve of bpy.types.Object the copy of the given object. """ new_ob = bobj.copy() new_ob.data = bobj.data.copy() if collection: collection.objects.link(new_ob) return new_ob
[docs]def set_matrix_world(bobj, matrix): """ Set the `matrix_world` of `bobj` to `matrix`. Parameters ---------- bobj : bpy.types.Object Blender object to set `matrix_world`. matrix : numpy.ndarray or mathutils.Matrix 4x4 matrix. Raises ------ ValueError If dimensions of matrix are not 4x4. If type of matrix is not numpy.ndarray or mathutils.Matrix. """ if not (len(matrix) == 4 and len(matrix[0]) == 4): ValueError("Matrix has wrong size. Expecting 4x4 matrix.") if isinstance(matrix, np.ndarray): # workaround for Blender bug when converting matrix from numpy bobj.matrix_world = matrix.transpose() elif isinstance(matrix, mathutils.Matrix): bobj.matrix_world = matrix else: raise ValueError( f"Wrong type {type(matrix)}. Expecting mathutils.Matrix or numpy.ndarray" )
[docs]def matrix_world_transformation_function(matrix): """ Creates a function that transforms Blender objects by a matrix. Parameters ---------- matrix : numpy.ndarray or mathutils.Matrix 4x4 matrix. Returns ------- object_trafo : Python function """ def object_trafo(bobj): set_matrix_world(bobj, np.array(bobj.matrix_world) @ np.array(matrix)) return object_trafo
[docs]def set_prop(bobj, prop, val): """ Set a single property of a Blender object. Parameters ---------- bobj : bpy.types.Object Blender object to set prperty to. prop : str Name of the property. val : any New value of the property. """ if prop == "matrix_world": trafo = matrix_world_transformation_function(val) bobj = trafo(bobj) else: setattr(bobj, prop, val)
[docs]def look_at_point( bobj, point, bobj_front=(0, -1, 0), bobj_up=(0, 0, 1), world_up=(0, 0, 1), distance=None, atol=None, rtol=None, ): """ Rotates an object such that it looks at the given point. If a distance is given, the object is additionally translated along the viewing direction to match the given distance to the point. If no distance is given, the object remains at its position. The default directions are set to fit the Suzanne object. Parameters ---------- bobj : bpy.types.Object Blenders object to move. point : iterable of 3 floats Point of reference to turn object. bobj_front : iterable of 3 floats (default=(0,-1,0)) Viewing direction of the object in the object space. That is the direction in which the object is looking in the object space. bobj_up : iterable of 3 floats (default=(0,0,1)) Up direction of the object in the object space. That is where the top is situed in the object space. world_up : iterable of 3 floats (default=(0,0,1)) Where the up direction should end up. distance: float, default=None Distance the object should have from the point. atol, rtol : float, default=None Tolerances used to determine whether *object* is located at *point*. If None, the global defaults will be used. Raises ------ ValueError If the object is located at the target point. """ # Intitialize variables. point = np.array(point) location = np.array(bobj.location) bobj_front = np.array(bobj_front) bobj_up = np.array(bobj_up) if nonexact.isclose(point, location, atol=atol, rtol=rtol).all(): raise ValueError("Object is located at target point") # Compute rotation (in canonical basis). front = normalize(point - location) right = normalize(np.cross(front, world_up)) if all(right == 0): # Gram schmidt to find a vector orthogonal to front. A = np.hstack([np.array(v).reshape(-1, 1) for v in [front, np.zeros(3)]]) Q = np.linalg.qr(A)[0] right = Q[:, 1] up = normalize(np.cross(right, front)) # Compute basis change (from object to canonical). bobj_right = normalize(np.cross(bobj_front, bobj_up)) bobj_matrix = np.hstack( [v.reshape(-1, 1) for v in [bobj_right, bobj_front, bobj_up]] ) bobj_matrix_inv = np.linalg.inv(bobj_matrix) # Apply transformation. bpy.context.view_layer.update() # Make sure matrix_world is up to date. mw = np.array(bobj.matrix_world) mw[:-1, 0] = right mw[:-1, 1] = front mw[:-1, 2] = up mw[:3, :3] = mw[:3, :3] @ bobj_matrix_inv if distance is not None: # Adjust translation to match distance. mw[:-1, 3] = point - front * distance bobj.matrix_world = mw.transpose()
[docs]def empty( collection=None, location=np.array((0, 0, 0)), empty_type="PLAIN_AXES", name="root", parent=None, ): """ Creates an empty blender object. Parameters ---------- collection: bpy.types.Collection (default=None) Links copy to the given collection. If `None`, object will not be linked. location: numpy.ndarray (default=np.array((0, 0, 0))) location of empty object. empty_type: str (default="PLAIN_AXES") Viewport display style for empty object. name: str (default="root") Name of empty object. parent: bpy.types.Object (default=None) Parent of empty object. Returns ------- bpy.types.Object """ root = bpy.data.objects.new(name, None) root.empty_display_type = empty_type root.location = location root.parent = parent if collection: collection.objects.link(root) return root
[docs]def clear(objects=None, do_unlink=True, deep=True): """ Delete all given objects. Parameters ---------- objects : Iterable of bpy.type.Object (default=None) Objects to be deleted. If the argument is not provided or `None`, all objects will be deleted. do_unlink : bool (default=True) Unlink objects from their collections, if needed, before deleting them. deep : bool (default=True) Delete the data corresponding to the object. """ # import at the top causes an circular import error import ddg.visualization.blender.curve import ddg.visualization.blender.mesh if objects is None: objects = bpy.data.objects objects = set(objects) if not deep: for obj in objects: if obj in set(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=do_unlink) return # Intersect with objects in the scene. We have to do this because # the set might contain invalid objects: This happens when objects # share data and we remove the data. objects_ = objects & set(bpy.data.objects) for obj in objects_: if obj not in set(bpy.data.objects): continue type_ = obj.type if type_ == "EMPTY": bpy.data.objects.remove(obj, do_unlink=do_unlink) elif type_ == "MESH": ddg.visualization.blender.mesh.clear(meshes=[obj.data]) elif type_ == "LIGHT": ddg.visualization.blender.light.clear(lights=[obj.data]) elif type_ == "CURVE": ddg.visualization.blender.curve.clear(curves=[obj.data]) elif type_ == "CAMERA": ddg.visualization.blender.camera.clear(cameras=[obj.data]) elif type_ == "LATTICE": clear_lattices(lattices=[obj.data]) else: raise NotImplementedError
[docs]def clear_empty_objects(do_unlink=True, deep=True): """ Delete all empty objects. Parameters ---------- do_unlink : bool (default=True) Unlink objects from their collections, if needed, before deleting them. deep : bool (default=True) Delete the data corresponding to the object. """ objects = set([object_ for object_ in bpy.data.objects if object_.type == "EMPTY"]) if not deep: for obj in objects: if obj in set(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=do_unlink) return # Intersect with objects in the scene. We have to do this because # the set might contain invalid objects: This happens when objects # share data and we remove the data. objects_ = objects & set(bpy.data.objects) for obj in objects_: if obj not in set(bpy.data.objects): continue bpy.data.objects.remove(obj, do_unlink=do_unlink)
[docs]def clear_lattices(lattices=None, do_unlink=True): """ Delete all given lattices. Parameters ---------- lattices : Iterable of bpy.type.Lattice (default=None) Lattices to be deleted. If the argument is not provided or `None`, all lattices will be deleted. do_unlink : bool (default=True) Unlink lattices from their scenes, if needed, before deleting them. """ if lattices is None: lattices = bpy.data.lattices lattices = list(lattices) while lattices: bpy.data.lattices.remove(lattices.pop(), do_unlink=do_unlink)
[docs]def get_data(x): """Return the data attribute if it exists or the input itself. Parameters ---------- x : Any Returns ------- Any Examples -------- >>> import bpy >>> from ddg.visualization.blender.object import get_data >>> >>> bpy.ops.mesh.primitive_cube_add() {'FINISHED'} >>> data = bpy.context.object.data >>> assert data == get_data(bpy.context.object) >>> assert data == get_data(bpy.context.object.data) """ if hasattr(x, "data"): return x.data else: return x