rigidbody.py 11 KB

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