"""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)