1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
|
# SPDX-FileCopyrightText: 2013-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Operator
from bpy.props import (
EnumProperty,
IntProperty,
)
class CopyRigidbodySettings(Operator):
"""Copy Rigid Body settings from active object to selected"""
bl_idname = "rigidbody.object_settings_copy"
bl_label = "Copy Rigid Body Settings"
bl_options = {'REGISTER', 'UNDO'}
_attrs = (
"type",
"kinematic",
"mass",
"collision_shape",
"use_margin",
"collision_margin",
"friction",
"restitution",
"use_deactivation",
"use_start_deactivated",
"deactivate_linear_velocity",
"deactivate_angular_velocity",
"linear_damping",
"angular_damping",
"collision_collections",
"mesh_source",
"use_deform",
"enabled",
)
@classmethod
def poll(cls, context):
obj = context.object
return (obj and obj.rigid_body)
def execute(self, context):
obj_act = context.object
# Deselect all non mesh objects and objects that
# already have a rigid body attached.
rb_objects = []
for o in context.selected_objects:
if o.type != 'MESH' or o.rigid_body is not None:
o.select_set(False)
if o.rigid_body is not None:
rb_objects.append(o)
bpy.ops.rigidbody.objects_add()
# Ensure that the rigid body objects
# we've de-selected are selected again.
for o in rb_objects:
o.select_set(True)
objects = context.selected_objects
if objects:
rb_from = obj_act.rigid_body
# copy settings
for o in objects:
rb_to = o.rigid_body
if o == obj_act:
continue
for attr in self._attrs:
setattr(rb_to, attr, getattr(rb_from, attr))
return {'FINISHED'}
class BakeToKeyframes(Operator):
"""Bake rigid body transformations of selected objects to keyframes"""
bl_idname = "rigidbody.bake_to_keyframes"
bl_label = "Bake to Keyframes"
bl_options = {'REGISTER', 'UNDO'}
frame_start: IntProperty(
name="Start Frame",
description="Start frame for baking",
min=0, max=300000,
default=1,
)
frame_end: IntProperty(
name="End Frame",
description="End frame for baking",
min=1, max=300000,
default=250,
)
step: IntProperty(
name="Frame Step",
description="Frame Step",
min=1, max=120,
default=1,
)
@classmethod
def poll(cls, context):
obj = context.object
return (obj and obj.rigid_body)
def execute(self, context):
bake = []
objects = []
scene = context.scene
frame_orig = scene.frame_current
frames_step = range(self.frame_start, self.frame_end + 1, self.step)
frames_full = range(self.frame_start, self.frame_end + 1)
# filter objects selection
for obj in context.selected_objects:
if not obj.rigid_body or obj.rigid_body.type != 'ACTIVE':
obj.select_set(False)
objects = context.selected_objects
if objects:
# store transformation data
# need to start at scene start frame so simulation is run from the beginning
for f in frames_full:
scene.frame_set(f)
if f in frames_step:
mat = {}
for i, obj in enumerate(objects):
mat[i] = obj.matrix_world.copy()
bake.append(mat)
# apply transformations as keyframes
for i, f in enumerate(frames_step):
scene.frame_set(f)
for j, obj in enumerate(objects):
mat = bake[i][j]
# Convert world space transform to parent space, so parented objects don't get offset after baking.
if obj.parent:
mat = obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ mat
obj.location = mat.to_translation()
rot_mode = obj.rotation_mode
if rot_mode == 'QUATERNION':
q1 = obj.rotation_quaternion
q2 = mat.to_quaternion()
# make quaternion compatible with the previous one
if q1.dot(q2) < 0.0:
obj.rotation_quaternion = -q2
else:
obj.rotation_quaternion = q2
elif rot_mode == 'AXIS_ANGLE':
# this is a little roundabout but there's no better way right now
aa = mat.to_quaternion().to_axis_angle()
obj.rotation_axis_angle = (aa[1], *aa[0])
else: # euler
# make sure euler rotation is compatible to previous frame
# NOTE: assume that on first frame, the starting rotation is appropriate
obj.rotation_euler = mat.to_euler(rot_mode, obj.rotation_euler)
bpy.ops.anim.keyframe_insert_by_name(type='BUILTIN_KSI_LocRot')
# remove baked objects from simulation
bpy.ops.rigidbody.objects_remove()
# clean up keyframes
for obj in objects:
action = obj.animation_data.action
for fcu in action.fcurves:
keyframe_points = fcu.keyframe_points
i = 1
# remove unneeded keyframes
while i < len(keyframe_points) - 1:
val_prev = keyframe_points[i - 1].co[1]
val_next = keyframe_points[i + 1].co[1]
val = keyframe_points[i].co[1]
if abs(val - val_prev) + abs(val - val_next) < 0.0001:
keyframe_points.remove(keyframe_points[i])
else:
i += 1
# use linear interpolation for better visual results
for keyframe in keyframe_points:
keyframe.interpolation = 'LINEAR'
# return to the frame we started on
scene.frame_set(frame_orig)
return {'FINISHED'}
def invoke(self, context, _event):
scene = context.scene
self.frame_start = scene.frame_start
self.frame_end = scene.frame_end
wm = context.window_manager
return wm.invoke_props_dialog(self)
class ConnectRigidBodies(Operator):
"""Create rigid body constraints between selected rigid bodies"""
bl_idname = "rigidbody.connect"
bl_label = "Connect Rigid Bodies"
bl_options = {'REGISTER', 'UNDO'}
con_type: EnumProperty(
name="Type",
description="Type of generated constraint",
# XXX Would be nice to get icons too, but currently not possible ;)
items=tuple(
(e.identifier, e.name, e.description, e. value)
for e in bpy.types.RigidBodyConstraint.bl_rna.properties["type"].enum_items
),
default='FIXED',
)
pivot_type: EnumProperty(
name="Location",
description="Constraint pivot location",
items=(
('CENTER', "Center", "Pivot location is between the constrained rigid bodies"),
('ACTIVE', "Active", "Pivot location is at the active object position"),
('SELECTED', "Selected", "Pivot location is at the selected object position"),
),
default='CENTER',
)
connection_pattern: EnumProperty(
name="Connection Pattern",
description="Pattern used to connect objects",
items=(
('SELECTED_TO_ACTIVE', "Selected to Active", "Connect selected objects to the active object"),
('CHAIN_DISTANCE', "Chain by Distance", "Connect objects as a chain based on distance, "
"starting at the active object"),
),
default='SELECTED_TO_ACTIVE',
)
@classmethod
def poll(cls, context):
obj = context.object
return (obj and obj.rigid_body)
def _add_constraint(self, context, object1, object2):
if object1 == object2:
return
if self.pivot_type == 'ACTIVE':
loc = object1.location
elif self.pivot_type == 'SELECTED':
loc = object2.location
else:
loc = (object1.location + object2.location) / 2.0
ob = bpy.data.objects.new("Constraint", object_data=None)
ob.location = loc
context.scene.collection.objects.link(ob)
context.view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.rigidbody.constraint_add()
con_obj = context.active_object
con_obj.empty_display_type = 'ARROWS'
con = con_obj.rigid_body_constraint
con.type = self.con_type
con.object1 = object1
con.object2 = object2
def execute(self, context):
view_layer = context.view_layer
objects = context.selected_objects
obj_act = context.active_object
change = False
if self.connection_pattern == 'CHAIN_DISTANCE':
objs_sorted = [obj_act]
objects_tmp = context.selected_objects
try:
objects_tmp.remove(obj_act)
except ValueError:
pass
last_obj = obj_act
while objects_tmp:
objects_tmp.sort(key=lambda o: (last_obj.location - o.location).length)
last_obj = objects_tmp.pop(0)
objs_sorted.append(last_obj)
for i in range(1, len(objs_sorted)):
self._add_constraint(context, objs_sorted[i - 1], objs_sorted[i])
change = True
else: # SELECTED_TO_ACTIVE
for obj in objects:
self._add_constraint(context, obj_act, obj)
change = True
if change:
# restore selection
bpy.ops.object.select_all(action='DESELECT')
for obj in objects:
obj.select_set(True)
view_layer.objects.active = obj_act
return {'FINISHED'}
else:
self.report({'WARNING'}, "No other objects selected")
return {'CANCELLED'}
classes = (
BakeToKeyframes,
ConnectRigidBodies,
CopyRigidbodySettings,
)
|