glTF exporter: Reset pose bone between each action
[blender-addons.git] / io_anim_bvh / import_bvh.py
blob6fcb5067f738b5587dc6686f983b38ffc985fd4f
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) Campbell Barton
5 from math import radians, ceil
7 import bpy
8 from mathutils import Vector, Euler, Matrix
11 class BVH_Node:
12 __slots__ = (
13 # Bvh joint name.
14 'name',
15 # BVH_Node type or None for no parent.
16 'parent',
17 # A list of children of this type..
18 'children',
19 # Worldspace rest location for the head of this node.
20 'rest_head_world',
21 # Localspace rest location for the head of this node.
22 'rest_head_local',
23 # Worldspace rest location for the tail of this node.
24 'rest_tail_world',
25 # Worldspace rest location for the tail of this node.
26 'rest_tail_local',
27 # List of 6 ints, -1 for an unused channel,
28 # otherwise an index for the BVH motion data lines,
29 # loc triple then rot triple.
30 'channels',
31 # A triple of indices as to the order rotation is applied.
32 # [0,1,2] is x/y/z - [None, None, None] if no rotation..
33 'rot_order',
34 # Same as above but a string 'XYZ' format..
35 'rot_order_str',
36 # A list one tuple's one for each frame: (locx, locy, locz, rotx, roty, rotz),
37 # euler rotation ALWAYS stored xyz order, even when native used.
38 'anim_data',
39 # Convenience function, bool, same as: (channels[0] != -1 or channels[1] != -1 or channels[2] != -1).
40 'has_loc',
41 # Convenience function, bool, same as: (channels[3] != -1 or channels[4] != -1 or channels[5] != -1).
42 'has_rot',
43 # Index from the file, not strictly needed but nice to maintain order.
44 'index',
45 # Use this for whatever you want.
46 'temp',
49 _eul_order_lookup = {
50 (None, None, None): 'XYZ', # XXX Dummy one, no rotation anyway!
51 (0, 1, 2): 'XYZ',
52 (0, 2, 1): 'XZY',
53 (1, 0, 2): 'YXZ',
54 (1, 2, 0): 'YZX',
55 (2, 0, 1): 'ZXY',
56 (2, 1, 0): 'ZYX',
59 def __init__(self, name, rest_head_world, rest_head_local, parent, channels, rot_order, index):
60 self.name = name
61 self.rest_head_world = rest_head_world
62 self.rest_head_local = rest_head_local
63 self.rest_tail_world = None
64 self.rest_tail_local = None
65 self.parent = parent
66 self.channels = channels
67 self.rot_order = tuple(rot_order)
68 self.rot_order_str = BVH_Node._eul_order_lookup[self.rot_order]
69 self.index = index
71 # convenience functions
72 self.has_loc = channels[0] != -1 or channels[1] != -1 or channels[2] != -1
73 self.has_rot = channels[3] != -1 or channels[4] != -1 or channels[5] != -1
75 self.children = []
77 # List of 6 length tuples: (lx, ly, lz, rx, ry, rz)
78 # even if the channels aren't used they will just be zero.
79 self.anim_data = [(0, 0, 0, 0, 0, 0)]
81 def __repr__(self):
82 return (
83 "BVH name: '%s', rest_loc:(%.3f,%.3f,%.3f), rest_tail:(%.3f,%.3f,%.3f)" % (
84 self.name,
85 *self.rest_head_world,
86 *self.rest_head_world,
91 def sorted_nodes(bvh_nodes):
92 bvh_nodes_list = list(bvh_nodes.values())
93 bvh_nodes_list.sort(key=lambda bvh_node: bvh_node.index)
94 return bvh_nodes_list
97 def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0):
98 # File loading stuff
99 # Open the file for importing
100 file = open(file_path, 'rU')
102 # Separate into a list of lists, each line a list of words.
103 file_lines = file.readlines()
104 # Non standard carrage returns?
105 if len(file_lines) == 1:
106 file_lines = file_lines[0].split('\r')
108 # Split by whitespace.
109 file_lines = [ll for ll in [l.split() for l in file_lines] if ll]
111 # Create hierarchy as empties
112 if file_lines[0][0].lower() == 'hierarchy':
113 # print 'Importing the BVH Hierarchy for:', file_path
114 pass
115 else:
116 raise Exception("This is not a BVH file")
118 bvh_nodes = {None: None}
119 bvh_nodes_serial = [None]
120 bvh_frame_count = None
121 bvh_frame_time = None
123 channelIndex = -1
125 lineIdx = 0 # An index for the file.
126 while lineIdx < len(file_lines) - 1:
127 if file_lines[lineIdx][0].lower() in {'root', 'joint'}:
129 # Join spaces into 1 word with underscores joining it.
130 if len(file_lines[lineIdx]) > 2:
131 file_lines[lineIdx][1] = '_'.join(file_lines[lineIdx][1:])
132 file_lines[lineIdx] = file_lines[lineIdx][:2]
134 # MAY NEED TO SUPPORT MULTIPLE ROOTS HERE! Still unsure weather multiple roots are possible?
136 # Make sure the names are unique - Object names will match joint names exactly and both will be unique.
137 name = file_lines[lineIdx][1]
139 # print '%snode: %s, parent: %s' % (len(bvh_nodes_serial) * ' ', name, bvh_nodes_serial[-1])
141 lineIdx += 2 # Increment to the next line (Offset)
142 rest_head_local = global_scale * Vector((
143 float(file_lines[lineIdx][1]),
144 float(file_lines[lineIdx][2]),
145 float(file_lines[lineIdx][3]),
147 lineIdx += 1 # Increment to the next line (Channels)
149 # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation]
150 # newChannel references indices to the motiondata,
151 # if not assigned then -1 refers to the last value that will be added on loading at a value of zero, this is appended
152 # We'll add a zero value onto the end of the MotionDATA so this always refers to a value.
153 my_channel = [-1, -1, -1, -1, -1, -1]
154 my_rot_order = [None, None, None]
155 rot_count = 0
156 for channel in file_lines[lineIdx][2:]:
157 channel = channel.lower()
158 channelIndex += 1 # So the index points to the right channel
159 if channel == 'xposition':
160 my_channel[0] = channelIndex
161 elif channel == 'yposition':
162 my_channel[1] = channelIndex
163 elif channel == 'zposition':
164 my_channel[2] = channelIndex
166 elif channel == 'xrotation':
167 my_channel[3] = channelIndex
168 my_rot_order[rot_count] = 0
169 rot_count += 1
170 elif channel == 'yrotation':
171 my_channel[4] = channelIndex
172 my_rot_order[rot_count] = 1
173 rot_count += 1
174 elif channel == 'zrotation':
175 my_channel[5] = channelIndex
176 my_rot_order[rot_count] = 2
177 rot_count += 1
179 channels = file_lines[lineIdx][2:]
181 my_parent = bvh_nodes_serial[-1] # account for none
183 # Apply the parents offset accumulatively
184 if my_parent is None:
185 rest_head_world = Vector(rest_head_local)
186 else:
187 rest_head_world = my_parent.rest_head_world + rest_head_local
189 bvh_node = bvh_nodes[name] = BVH_Node(
190 name,
191 rest_head_world,
192 rest_head_local,
193 my_parent,
194 my_channel,
195 my_rot_order,
196 len(bvh_nodes) - 1,
199 # If we have another child then we can call ourselves a parent, else
200 bvh_nodes_serial.append(bvh_node)
202 # Account for an end node.
203 # There is sometimes a name after 'End Site' but we will ignore it.
204 if file_lines[lineIdx][0].lower() == 'end' and file_lines[lineIdx][1].lower() == 'site':
205 # Increment to the next line (Offset)
206 lineIdx += 2
207 rest_tail = global_scale * Vector((
208 float(file_lines[lineIdx][1]),
209 float(file_lines[lineIdx][2]),
210 float(file_lines[lineIdx][3]),
213 bvh_nodes_serial[-1].rest_tail_world = bvh_nodes_serial[-1].rest_head_world + rest_tail
214 bvh_nodes_serial[-1].rest_tail_local = bvh_nodes_serial[-1].rest_head_local + rest_tail
216 # Just so we can remove the parents in a uniform way,
217 # the end has kids so this is a placeholder.
218 bvh_nodes_serial.append(None)
220 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0] == '}': # == ['}']
221 bvh_nodes_serial.pop() # Remove the last item
223 # End of the hierarchy. Begin the animation section of the file with
224 # the following header.
225 # MOTION
226 # Frames: n
227 # Frame Time: dt
228 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0].lower() == 'motion':
229 lineIdx += 1 # Read frame count.
230 if (
231 len(file_lines[lineIdx]) == 2 and
232 file_lines[lineIdx][0].lower() == 'frames:'
234 bvh_frame_count = int(file_lines[lineIdx][1])
236 lineIdx += 1 # Read frame rate.
237 if (
238 len(file_lines[lineIdx]) == 3 and
239 file_lines[lineIdx][0].lower() == 'frame' and
240 file_lines[lineIdx][1].lower() == 'time:'
242 bvh_frame_time = float(file_lines[lineIdx][2])
244 lineIdx += 1 # Set the cursor to the first frame
246 break
248 lineIdx += 1
250 # Remove the None value used for easy parent reference
251 del bvh_nodes[None]
252 # Don't use anymore
253 del bvh_nodes_serial
255 # importing world with any order but nicer to maintain order
256 # second life expects it, which isn't to spec.
257 bvh_nodes_list = sorted_nodes(bvh_nodes)
259 while lineIdx < len(file_lines):
260 line = file_lines[lineIdx]
261 for bvh_node in bvh_nodes_list:
262 # for bvh_node in bvh_nodes_serial:
263 lx = ly = lz = rx = ry = rz = 0.0
264 channels = bvh_node.channels
265 anim_data = bvh_node.anim_data
266 if channels[0] != -1:
267 lx = global_scale * float(line[channels[0]])
269 if channels[1] != -1:
270 ly = global_scale * float(line[channels[1]])
272 if channels[2] != -1:
273 lz = global_scale * float(line[channels[2]])
275 if channels[3] != -1 or channels[4] != -1 or channels[5] != -1:
277 rx = radians(float(line[channels[3]]))
278 ry = radians(float(line[channels[4]]))
279 rz = radians(float(line[channels[5]]))
281 # Done importing motion data #
282 anim_data.append((lx, ly, lz, rx, ry, rz))
283 lineIdx += 1
285 # Assign children
286 for bvh_node in bvh_nodes_list:
287 bvh_node_parent = bvh_node.parent
288 if bvh_node_parent:
289 bvh_node_parent.children.append(bvh_node)
291 # Now set the tip of each bvh_node
292 for bvh_node in bvh_nodes_list:
294 if not bvh_node.rest_tail_world:
295 if len(bvh_node.children) == 0:
296 # could just fail here, but rare BVH files have childless nodes
297 bvh_node.rest_tail_world = Vector(bvh_node.rest_head_world)
298 bvh_node.rest_tail_local = Vector(bvh_node.rest_head_local)
299 elif len(bvh_node.children) == 1:
300 bvh_node.rest_tail_world = Vector(bvh_node.children[0].rest_head_world)
301 bvh_node.rest_tail_local = bvh_node.rest_head_local + bvh_node.children[0].rest_head_local
302 else:
303 # allow this, see above
304 # if not bvh_node.children:
305 # raise Exception("bvh node has no end and no children. bad file")
307 # Removed temp for now
308 rest_tail_world = Vector((0.0, 0.0, 0.0))
309 rest_tail_local = Vector((0.0, 0.0, 0.0))
310 for bvh_node_child in bvh_node.children:
311 rest_tail_world += bvh_node_child.rest_head_world
312 rest_tail_local += bvh_node_child.rest_head_local
314 bvh_node.rest_tail_world = rest_tail_world * (1.0 / len(bvh_node.children))
315 bvh_node.rest_tail_local = rest_tail_local * (1.0 / len(bvh_node.children))
317 # Make sure tail isn't the same location as the head.
318 if (bvh_node.rest_tail_local - bvh_node.rest_head_local).length <= 0.001 * global_scale:
319 print("\tzero length node found:", bvh_node.name)
320 bvh_node.rest_tail_local.y = bvh_node.rest_tail_local.y + global_scale / 10
321 bvh_node.rest_tail_world.y = bvh_node.rest_tail_world.y + global_scale / 10
323 return bvh_nodes, bvh_frame_time, bvh_frame_count
326 def bvh_node_dict2objects(context, bvh_name, bvh_nodes, rotate_mode='NATIVE', frame_start=1, IMPORT_LOOP=False):
328 if frame_start < 1:
329 frame_start = 1
331 scene = context.scene
332 for obj in scene.objects:
333 obj.select_set(False)
335 objects = []
337 def add_ob(name):
338 obj = bpy.data.objects.new(name, None)
339 context.collection.objects.link(obj)
340 objects.append(obj)
341 obj.select_set(True)
343 # nicer drawing.
344 obj.empty_display_type = 'CUBE'
345 obj.empty_display_size = 0.1
347 return obj
349 # Add objects
350 for name, bvh_node in bvh_nodes.items():
351 bvh_node.temp = add_ob(name)
352 bvh_node.temp.rotation_mode = bvh_node.rot_order_str[::-1]
354 # Parent the objects
355 for bvh_node in bvh_nodes.values():
356 for bvh_node_child in bvh_node.children:
357 bvh_node_child.temp.parent = bvh_node.temp
359 # Offset
360 for bvh_node in bvh_nodes.values():
361 # Make relative to parents offset
362 bvh_node.temp.location = bvh_node.rest_head_local
364 # Add tail objects
365 for name, bvh_node in bvh_nodes.items():
366 if not bvh_node.children:
367 ob_end = add_ob(name + '_end')
368 ob_end.parent = bvh_node.temp
369 ob_end.location = bvh_node.rest_tail_world - bvh_node.rest_head_world
371 for name, bvh_node in bvh_nodes.items():
372 obj = bvh_node.temp
374 for frame_current in range(len(bvh_node.anim_data)):
376 lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current]
378 if bvh_node.has_loc:
379 obj.delta_location = Vector((lx, ly, lz)) - bvh_node.rest_head_world
380 obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current)
382 if bvh_node.has_rot:
383 obj.delta_rotation_euler = rx, ry, rz
384 obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current)
386 return objects
389 def bvh_node_dict2armature(
390 context,
391 bvh_name,
392 bvh_nodes,
393 bvh_frame_time,
394 rotate_mode='XYZ',
395 frame_start=1,
396 IMPORT_LOOP=False,
397 global_matrix=None,
398 use_fps_scale=False,
401 if frame_start < 1:
402 frame_start = 1
404 # Add the new armature,
405 scene = context.scene
406 for obj in scene.objects:
407 obj.select_set(False)
409 arm_data = bpy.data.armatures.new(bvh_name)
410 arm_ob = bpy.data.objects.new(bvh_name, arm_data)
412 context.collection.objects.link(arm_ob)
414 arm_ob.select_set(True)
415 context.view_layer.objects.active = arm_ob
417 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
418 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
420 bvh_nodes_list = sorted_nodes(bvh_nodes)
422 # Get the average bone length for zero length bones, we may not use this.
423 average_bone_length = 0.0
424 nonzero_count = 0
425 for bvh_node in bvh_nodes_list:
426 l = (bvh_node.rest_head_local - bvh_node.rest_tail_local).length
427 if l:
428 average_bone_length += l
429 nonzero_count += 1
431 # Very rare cases all bones could be zero length???
432 if not average_bone_length:
433 average_bone_length = 0.1
434 else:
435 # Normal operation
436 average_bone_length = average_bone_length / nonzero_count
438 # XXX, annoying, remove bone.
439 while arm_data.edit_bones:
440 arm_ob.edit_bones.remove(arm_data.edit_bones[-1])
442 ZERO_AREA_BONES = []
443 for bvh_node in bvh_nodes_list:
445 # New editbone
446 bone = bvh_node.temp = arm_data.edit_bones.new(bvh_node.name)
448 bone.head = bvh_node.rest_head_world
449 bone.tail = bvh_node.rest_tail_world
451 # Zero Length Bones! (an exceptional case)
452 if (bone.head - bone.tail).length < 0.001:
453 print("\tzero length bone found:", bone.name)
454 if bvh_node.parent:
455 ofs = bvh_node.parent.rest_head_local - bvh_node.parent.rest_tail_local
456 if ofs.length: # is our parent zero length also?? unlikely
457 bone.tail = bone.tail - ofs
458 else:
459 bone.tail.y = bone.tail.y + average_bone_length
460 else:
461 bone.tail.y = bone.tail.y + average_bone_length
463 ZERO_AREA_BONES.append(bone.name)
465 for bvh_node in bvh_nodes_list:
466 if bvh_node.parent:
467 # bvh_node.temp is the Editbone
469 # Set the bone parent
470 bvh_node.temp.parent = bvh_node.parent.temp
472 # Set the connection state
474 (not bvh_node.has_loc) and
475 (bvh_node.parent.temp.name not in ZERO_AREA_BONES) and
476 (bvh_node.parent.rest_tail_local == bvh_node.rest_head_local)
478 bvh_node.temp.use_connect = True
480 # Replace the editbone with the editbone name,
481 # to avoid memory errors accessing the editbone outside editmode
482 for bvh_node in bvh_nodes_list:
483 bvh_node.temp = bvh_node.temp.name
485 # Now Apply the animation to the armature
487 # Get armature animation data
488 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
490 pose = arm_ob.pose
491 pose_bones = pose.bones
493 if rotate_mode == 'NATIVE':
494 for bvh_node in bvh_nodes_list:
495 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
496 pose_bone = pose_bones[bone_name]
497 pose_bone.rotation_mode = bvh_node.rot_order_str
499 elif rotate_mode != 'QUATERNION':
500 for pose_bone in pose_bones:
501 pose_bone.rotation_mode = rotate_mode
502 else:
503 # Quats default
504 pass
506 context.view_layer.update()
508 arm_ob.animation_data_create()
509 action = bpy.data.actions.new(name=bvh_name)
510 arm_ob.animation_data.action = action
512 # Replace the bvh_node.temp (currently an editbone)
513 # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv)
514 num_frame = 0
515 for bvh_node in bvh_nodes_list:
516 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
517 pose_bone = pose_bones[bone_name]
518 rest_bone = arm_data.bones[bone_name]
519 bone_rest_matrix = rest_bone.matrix_local.to_3x3()
521 bone_rest_matrix_inv = Matrix(bone_rest_matrix)
522 bone_rest_matrix_inv.invert()
524 bone_rest_matrix_inv.resize_4x4()
525 bone_rest_matrix.resize_4x4()
526 bvh_node.temp = (pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv)
528 if 0 == num_frame:
529 num_frame = len(bvh_node.anim_data)
531 # Choose to skip some frames at the beginning. Frame 0 is the rest pose
532 # used internally by this importer. Frame 1, by convention, is also often
533 # the rest pose of the skeleton exported by the motion capture system.
534 skip_frame = 1
535 if num_frame > skip_frame:
536 num_frame = num_frame - skip_frame
538 # Create a shared time axis for all animation curves.
539 time = [float(frame_start)] * num_frame
540 if use_fps_scale:
541 dt = scene.render.fps * bvh_frame_time
542 for frame_i in range(1, num_frame):
543 time[frame_i] += float(frame_i) * dt
544 else:
545 for frame_i in range(1, num_frame):
546 time[frame_i] += float(frame_i)
548 # print("bvh_frame_time = %f, dt = %f, num_frame = %d"
549 # % (bvh_frame_time, dt, num_frame]))
551 for i, bvh_node in enumerate(bvh_nodes_list):
552 pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp
554 if bvh_node.has_loc:
555 # Not sure if there is a way to query this or access it in the
556 # PoseBone structure.
557 data_path = 'pose.bones["%s"].location' % pose_bone.name
559 location = [(0.0, 0.0, 0.0)] * num_frame
560 for frame_i in range(num_frame):
561 bvh_loc = bvh_node.anim_data[frame_i + skip_frame][:3]
563 bone_translate_matrix = Matrix.Translation(
564 Vector(bvh_loc) - bvh_node.rest_head_local)
565 location[frame_i] = (bone_rest_matrix_inv @
566 bone_translate_matrix).to_translation()
568 # For each location x, y, z.
569 for axis_i in range(3):
570 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
571 keyframe_points = curve.keyframe_points
572 keyframe_points.add(num_frame)
574 for frame_i in range(num_frame):
575 keyframe_points[frame_i].co = (
576 time[frame_i],
577 location[frame_i][axis_i],
580 if bvh_node.has_rot:
581 data_path = None
582 rotate = None
584 if 'QUATERNION' == rotate_mode:
585 rotate = [(1.0, 0.0, 0.0, 0.0)] * num_frame
586 data_path = ('pose.bones["%s"].rotation_quaternion'
587 % pose_bone.name)
588 else:
589 rotate = [(0.0, 0.0, 0.0)] * num_frame
590 data_path = ('pose.bones["%s"].rotation_euler' %
591 pose_bone.name)
593 prev_euler = Euler((0.0, 0.0, 0.0))
594 for frame_i in range(num_frame):
595 bvh_rot = bvh_node.anim_data[frame_i + skip_frame][3:]
597 # apply rotation order and convert to XYZ
598 # note that the rot_order_str is reversed.
599 euler = Euler(bvh_rot, bvh_node.rot_order_str[::-1])
600 bone_rotation_matrix = euler.to_matrix().to_4x4()
601 bone_rotation_matrix = (
602 bone_rest_matrix_inv @
603 bone_rotation_matrix @
604 bone_rest_matrix
607 if len(rotate[frame_i]) == 4:
608 rotate[frame_i] = bone_rotation_matrix.to_quaternion()
609 else:
610 rotate[frame_i] = bone_rotation_matrix.to_euler(
611 pose_bone.rotation_mode, prev_euler)
612 prev_euler = rotate[frame_i]
614 # For each euler angle x, y, z (or quaternion w, x, y, z).
615 for axis_i in range(len(rotate[0])):
616 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
617 keyframe_points = curve.keyframe_points
618 keyframe_points.add(num_frame)
620 for frame_i in range(num_frame):
621 keyframe_points[frame_i].co = (
622 time[frame_i],
623 rotate[frame_i][axis_i],
626 for cu in action.fcurves:
627 if IMPORT_LOOP:
628 pass # 2.5 doenst have cyclic now?
630 for bez in cu.keyframe_points:
631 bez.interpolation = 'LINEAR'
633 # finally apply matrix
634 arm_ob.matrix_world = global_matrix
635 bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
637 return arm_ob
640 def load(
641 context,
642 filepath,
644 target='ARMATURE',
645 rotate_mode='NATIVE',
646 global_scale=1.0,
647 use_cyclic=False,
648 frame_start=1,
649 global_matrix=None,
650 use_fps_scale=False,
651 update_scene_fps=False,
652 update_scene_duration=False,
653 report=print,
655 import time
656 t1 = time.time()
657 print("\tparsing bvh %r..." % filepath, end="")
659 bvh_nodes, bvh_frame_time, bvh_frame_count = read_bvh(
660 context, filepath,
661 rotate_mode=rotate_mode,
662 global_scale=global_scale,
665 print("%.4f" % (time.time() - t1))
667 scene = context.scene
668 frame_orig = scene.frame_current
670 # Broken BVH handling: guess frame rate when it is not contained in the file.
671 if bvh_frame_time is None:
672 report(
673 {'WARNING'},
674 "The BVH file does not contain frame duration in its MOTION "
675 "section, assuming the BVH and Blender scene have the same "
676 "frame rate"
678 bvh_frame_time = scene.render.fps_base / scene.render.fps
679 # No need to scale the frame rate, as they're equal now anyway.
680 use_fps_scale = False
682 if update_scene_fps:
683 _update_scene_fps(context, report, bvh_frame_time)
685 # Now that we have a 1-to-1 mapping of Blender frames and BVH frames, there is no need
686 # to scale the FPS any more. It's even better not to, to prevent roundoff errors.
687 use_fps_scale = False
689 if update_scene_duration:
690 _update_scene_duration(context, report, bvh_frame_count, bvh_frame_time, frame_start, use_fps_scale)
692 t1 = time.time()
693 print("\timporting to blender...", end="")
695 bvh_name = bpy.path.display_name_from_filepath(filepath)
697 if target == 'ARMATURE':
698 bvh_node_dict2armature(
699 context, bvh_name, bvh_nodes, bvh_frame_time,
700 rotate_mode=rotate_mode,
701 frame_start=frame_start,
702 IMPORT_LOOP=use_cyclic,
703 global_matrix=global_matrix,
704 use_fps_scale=use_fps_scale,
707 elif target == 'OBJECT':
708 bvh_node_dict2objects(
709 context, bvh_name, bvh_nodes,
710 rotate_mode=rotate_mode,
711 frame_start=frame_start,
712 IMPORT_LOOP=use_cyclic,
713 # global_matrix=global_matrix, # TODO
716 else:
717 report({'ERROR'}, "Invalid target %r (must be 'ARMATURE' or 'OBJECT')" % target)
718 return {'CANCELLED'}
720 print('Done in %.4f\n' % (time.time() - t1))
722 context.scene.frame_set(frame_orig)
724 return {'FINISHED'}
727 def _update_scene_fps(context, report, bvh_frame_time):
728 """Update the scene's FPS settings from the BVH, but only if the BVH contains enough info."""
730 # Broken BVH handling: prevent division by zero.
731 if bvh_frame_time == 0.0:
732 report(
733 {'WARNING'},
734 "Unable to update scene frame rate, as the BVH file "
735 "contains a zero frame duration in its MOTION section",
737 return
739 scene = context.scene
740 scene_fps = scene.render.fps / scene.render.fps_base
741 new_fps = 1.0 / bvh_frame_time
743 if scene.render.fps != new_fps or scene.render.fps_base != 1.0:
744 print("\tupdating scene FPS (was %f) to BVH FPS (%f)" % (scene_fps, new_fps))
745 scene.render.fps = new_fps
746 scene.render.fps_base = 1.0
749 def _update_scene_duration(
750 context, report, bvh_frame_count, bvh_frame_time, frame_start,
751 use_fps_scale):
752 """Extend the scene's duration so that the BVH file fits in its entirety."""
754 if bvh_frame_count is None:
755 report(
756 {'WARNING'},
757 "Unable to extend the scene duration, as the BVH file does not "
758 "contain the number of frames in its MOTION section",
760 return
762 # Not likely, but it can happen when a BVH is just used to store an armature.
763 if bvh_frame_count == 0:
764 return
766 if use_fps_scale:
767 scene_fps = context.scene.render.fps / context.scene.render.fps_base
768 scaled_frame_count = int(ceil(bvh_frame_count * bvh_frame_time * scene_fps))
769 bvh_last_frame = frame_start + scaled_frame_count
770 else:
771 bvh_last_frame = frame_start + bvh_frame_count
773 # Only extend the scene, never shorten it.
774 if context.scene.frame_end < bvh_last_frame:
775 context.scene.frame_end = bvh_last_frame