Source code for ddg.visualization.blender.object

"""Collection of utility functions dealing with Blender objects

Most of the methods come in two flavors *add_* and *create_*

The create methods can be used to create objects containing meshes
without linking it to a scene. The corresponding add methods create the
objects containing the meshes first and also link it to the scene of the
current context.


"""

import bmesh
import bpy

import numpy as np
import mathutils

import ddg.visualization.blender.mesh as mutils
from ddg.visualization.blender.clear import clear_meshes
from ddg.math.euclidean import normalize


[docs]def from_data(data, name, parent=None, matrix=np.eye(4), link=True, collection=None): """ Create an object containing a mesh, optionally it links the created objected to the given collection. 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 : np.ndarray, (default=np.eye(4)) local transformation of the object link: bool, (default=True) True if object should be linked to collection collection : bpy.types.Collection, (default=None) collection to link the object to, if not given or None, the object will be linked to bpy.context.scene.collection 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 link: if collection is None: collection = bpy.context.scene.collection collection.objects.link(bobj) return bobj
[docs]def delete(ob): # TODO: thorough purge of all data!!! bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.ops.object.delete()
[docs]def copy(ob, collection=None): if collection is None: collection = ob.users_collection[0] new_ob = ob.copy() new_ob.data = ob.data.copy() collection.objects.link(new_ob) return new_ob
[docs]def add_joined(objects, name=None, keep_original=True, parent=None, matrix=np.eye(4), collection=None, remove_doubles=False): if collection is None: collection = objects[0].users_collection[0] bm = bmesh.new() if name is None: name = objects[0].name for ob in objects: mesh = ob.data mutils.transform(mesh, np.array(ob.matrix_world)) bm.from_mesh(mesh) if not keep_original: delete(ob) if remove_doubles: if remove_doubles is True: remove_doubles = 0.001 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=remove_doubles) mesh = mutils.from_bmesh(bm, name=name) ob = from_data(mesh, name=name, parent=parent, matrix=matrix, link=True, collection=collection) return ob
[docs]def add_connected_components(ob, keep_original=True): if keep_original: ob = copy(ob) bpy.context.view_layer.objects.active = ob ob.select_set(True) # bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.separate(type='LOOSE') # bpy.ops.object.mode_set(mode='OBJECT') components = bpy.context.selected_objects bpy.ops.object.select_all(action='DESELECT') return components
[docs]def selected_vertices_of_active_object(): mode = bpy.context.active_object.mode # we need to switch from Edit mode to Object mode so the selection gets updated bpy.ops.object.mode_set(mode='OBJECT') selectedVerts = [v for v in bpy.context.active_object.data.vertices if v.select] # back to whatever mode we were in bpy.ops.object.mode_set(mode=mode) return selectedVerts
[docs]def add_intersection_curve(ob1, ob2, name='IntersectionCurve', collection=None, convert_to_curve=True): """ Intersects to given objects. Warning: Your Edit mode needs to be set to select vertices, not edges or faces! Parameters ---------- ob1: bpy.types.Object First object to intersect ob2: bpy.types.Object Second object to intersect name: str, optional Name of the intersection object default = IntersectionCurve collection: bpy.types.Collection, optional Collection to add the newly created object to default = collection of ob1 convert_to_curve: Bool, optional Determines whether the created object is converted to a curve default = True Returns ------- bpy.types.Curve of bpy.types.Object """ if ob1.type != 'MESH' or ob2.type != 'MESH' or ob1.data is None or ob2.data is None: return None if collection is None: collection = ob1.users_collection[0] ob1_hide, ob1.hide_viewport = ob1.hide_viewport, False ob2_hide, ob2.hide_viewport = ob2.hide_viewport, False ob1_components = add_connected_components(ob1) ob2_components = add_connected_components(ob2) scene = bpy.context.scene curves = [] for ob1_comp in ob1_components: for ob2_comp in ob2_components: # Create new object from ob1_comp and ob2_comp ob = add_joined([ob1_comp, ob2_comp], name='', keep_original=True) # Add intersection and remove the rest bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.intersect(mode='SELECT') bpy.ops.mesh.select_all(action='INVERT') if len(selected_vertices_of_active_object()) == 0: # Case: No intersection found bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.delete(type='VERT') bpy.ops.object.mode_set(mode='OBJECT') curves.append(ob) # remove double points for curve in curves: bpy.context.view_layer.objects.active = curve bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(curve.data) bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=1e-2) bmesh.update_edit_mesh(curve.data) bpy.ops.object.mode_set(mode='OBJECT') # Delete components for ob1_comp in ob1_components: delete(ob1_comp) for ob2_comp in ob2_components: delete(ob2_comp) # Join intersection curves into one object (and convert to curve object) curve = add_joined(curves, collection=collection, name=name, keep_original=False) if len(curve.data.vertices) == 0: delete(curve) return None if convert_to_curve: bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = curve curve.select_set(True) bpy.ops.object.convert(target='CURVE') curve.select_set(False) clear_meshes(only_unused=True) ob1.hide_viewport, ob2.hide_viewport = ob1_hide, ob2_hide return curve
[docs]def set_matrix_world(bobj, matrix): """Set the matrix_world of bobj to matrix. Parameters ---------- m : numpy.ndarray or mathutils.Matrix 4x4 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_to_obj_trafo(m): """Given a matrix, return a function taking an object as parameter and applying the transformation corresponding to the matrix to it. Parameters ---------- m : numpy.ndarray or mathutils.Matrix 4x4 matrix. """ def object_trafo(ob): set_matrix_world(ob, np.array(ob.matrix_world) @ np.array(m)) return object_trafo
[docs]def create_duplicate_linked(bobj, transformations, use_blender_operator=False, collection=None): """Function creating linked duplicates of object and applying a transformation on each of them. Parameters ---------- bobj : bpy.types.Object Blender object to duplicate. transformations : list List of 4x4 matrices corresponding to the transformations. use_blender_operator : bool, default=False False : simulate the duplicate linked using bpy. True : use the "duplicate" operator of blender. collection : bpy.types.Collection, optional Collection in which to add the created duplicates. If not given, add to the collection of the original object. Returns ------- duplicates : list A list of blender objects : the created linked duplicates. """ transformation_functions = [matrix_to_obj_trafo(m) for m in transformations] duplicates = [] if not use_blender_operator: if not collection: collection = bobj.users_collection[0] for trafo in transformation_functions: new_mesh = bpy.data.meshes.new("tmp") duplicate = bpy.data.objects.new(bobj.name, new_mesh) duplicate.data = bobj.data collection.objects.link(duplicate) trafo(duplicate) duplicates.append(duplicate) bpy.data.meshes.remove(new_mesh) else: for trafo in transformation_functions: bpy.ops.object.select_all(action='DESELECT') bobj.select_set(True) bpy.ops.object.duplicate(linked=True) duplicate = bpy.context.selected_objects[0] trafo(duplicate) if collection: collection.objects.link(duplicate) duplicates.append(duplicate) bpy.ops.object.select_all(action='DESELECT') return duplicates
[docs]def look_at_point(obj, point, obj_front=(0, -1, 0), obj_up=(0, 0, 1), world_up=(0, 0, 1), distance=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 camera remains at its position. The default directions are set to fit the Suzanne object. Parameters ---------- obj: bpy.types.Object Blenders object to move. point: iterable of 3 floats Point of reference to turn object. obj_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. obj_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. Raises ------ ValueError If the object is located a the target point. """ # Intitialize variables. point = np.array(point) location = np.array(obj.location) obj_front = np.array(obj_front) obj_up = np.array(obj_up) if np.isclose(point, location).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). obj_right = normalize(np.cross(obj_front, obj_up)) obj_matrix = np.hstack([v.reshape(-1, 1) for v in [obj_right, obj_front, obj_up]]) obj_matrix_inv = np.linalg.inv(obj_matrix) # Apply transformation. bpy.context.view_layer.update() # Make sure matrix_world is up to date. mw = np.array(obj.matrix_world) mw[:-1, 0] = right mw[:-1, 1] = front mw[:-1, 2] = up mw[:3, :3] = mw[:3, :3] @ obj_matrix_inv if not distance is None: # Adjust translation to match distance. mw[:-1, 3] = point - front * distance obj.matrix_world = mw.transpose()
[docs]def get_child_names(obj): names = set() for child in obj.children: names.add(child.name) if child.children: names = names.union(get_child_names(child)) return names
[docs]def delete_hierarchy(obj): delete_children(obj) data = obj.data bpy.data.objects.remove(obj.name) if data.users == 0: if isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
[docs]def delete_children(obj): names = set().union(get_child_names(obj)) objects = bpy.data.objects for n in names: data = objects[n].data objects.remove(objects[n], do_unlink=True) if data.users == 0: if isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
[docs]def set_siblings_visibilities(obj, hide=False): siblings = select_siblings(obj, lambda x: True) for s in siblings: s.hide_set(hide, view_layer=bpy.context.view_layer)
[docs]def move_children_to_layers(obj, layer_indices=[0]): names = set([obj.name]).union(get_child_names(obj)) objects = bpy.data.objects layers = [i in layer_indices for i in range(20)] for n in names: objects[n].layers = layers
[docs]def select_first_sibling(obj, selection_function): selected_siblings = (bpy.data.objects[n] for n in get_child_names(obj) if selection_function(n)) return next(selected_siblings.__iter__())
[docs]def select_siblings(obj, selection_function): return (bpy.data.objects[n] for n in get_child_names(obj) if selection_function(n))
[docs]def select_children(obj, selection_function): return (o for o in obj.children if selection_function(o.name))
[docs]def move_to_layer(ob, layer): #add to layer first - needs to be on at least one layer ob.layers[layer] = True #wipe other layers for i in range(20): ob.layers[i] = (i == layer)