"""Collection of utility functions dealing with Blender objects
"""
import bpy
import mathutils
import numpy as np
import ddg
from ddg import nonexact
from ddg.math.euclidean import normalize
[docs]def blender_data_to_object(name, data):
"""Wrap a Blender data-block in a Blender object.
Parameters
----------
name : str
The `name` attribute of the object.
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 link(bobj, collection):
"""Link a Blender object to a collection.
Parameters
----------
bobj : bpy.types.Object
Blender object to be linked.
collection : bpy.types.Collection or str
Collection or name of collection to link the object to.
Notes
-----
If no collection exists with the given name, a new one will be created.
"""
if isinstance(collection, str):
col = ddg.blender.collection.collection(collection)
else:
col = collection
col.objects.link(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 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.
"""
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.blender.mesh.clear(meshes=[obj.data])
elif type_ == "LIGHT":
ddg.blender.light.clear(lights=[obj.data])
elif type_ == "CURVE":
ddg.blender.curve.clear(curves=[obj.data])
elif type_ == "CAMERA":
ddg.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.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