Fix T98039: Node Wrangler node preview no longer working
[blender-addons.git] / io_anim_bvh / import_bvh.py
blob2f335513c7255d98a4cb7e8374999e727e82d5e7
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 # Script copyright (C) Campbell Barton
7 from math import radians, ceil
9 import bpy
10 from mathutils import Vector, Euler, Matrix
13 class BVH_Node:
14 __slots__ = (
15 # Bvh joint name.
16 'name',
17 # BVH_Node type or None for no parent.
18 'parent',
19 # A list of children of this type..
20 'children',
21 # Worldspace rest location for the head of this node.
22 'rest_head_world',
23 # Localspace rest location for the head of this node.
24 'rest_head_local',
25 # Worldspace rest location for the tail of this node.
26 'rest_tail_world',
27 # Worldspace rest location for the tail of this node.
28 'rest_tail_local',
29 # List of 6 ints, -1 for an unused channel,
30 # otherwise an index for the BVH motion data lines,
31 # loc triple then rot triple.
32 'channels',
33 # A triple of indices as to the order rotation is applied.
34 # [0,1,2] is x/y/z - [None, None, None] if no rotation..
35 'rot_order',
36 # Same as above but a string 'XYZ' format..
37 'rot_order_str',
38 # A list one tuple's one for each frame: (locx, locy, locz, rotx, roty, rotz),
39 # euler rotation ALWAYS stored xyz order, even when native used.
40 'anim_data',
41 # Convenience function, bool, same as: (channels[0] != -1 or channels[1] != -1 or channels[2] != -1).
42 'has_loc',
43 # Convenience function, bool, same as: (channels[3] != -1 or channels[4] != -1 or channels[5] != -1).
44 'has_rot',
45 # Index from the file, not strictly needed but nice to maintain order.
46 'index',
47 # Use this for whatever you want.
48 'temp',
51 _eul_order_lookup = {
52 (None, None, None): 'XYZ', # XXX Dummy one, no rotation anyway!
53 (0, 1, 2): 'XYZ',
54 (0, 2, 1): 'XZY',
55 (1, 0, 2): 'YXZ',
56 (1, 2, 0): 'YZX',
57 (2, 0, 1): 'ZXY',
58 (2, 1, 0): 'ZYX',
61 def __init__(self, name, rest_head_world, rest_head_local, parent, channels, rot_order, index):
62 self.name = name
63 self.rest_head_world = rest_head_world
64 self.rest_head_local = rest_head_local
65 self.rest_tail_world = None
66 self.rest_tail_local = None
67 self.parent = parent
68 self.channels = channels
69 self.rot_order = tuple(rot_order)
70 self.rot_order_str = BVH_Node._eul_order_lookup[self.rot_order]
71 self.index = index
73 # convenience functions
74 self.has_loc = channels[0] != -1 or channels[1] != -1 or channels[2] != -1
75 self.has_rot = channels[3] != -1 or channels[4] != -1 or channels[5] != -1
77 self.children = []
79 # List of 6 length tuples: (lx, ly, lz, rx, ry, rz)
80 # even if the channels aren't used they will just be zero.
81 self.anim_data = [(0, 0, 0, 0, 0, 0)]
83 def __repr__(self):
84 return (
85 "BVH name: '%s', rest_loc:(%.3f,%.3f,%.3f), rest_tail:(%.3f,%.3f,%.3f)" % (
86 self.name,
87 *self.rest_head_world,
88 *self.rest_head_world,
93 def sorted_nodes(bvh_nodes):
94 bvh_nodes_list = list(bvh_nodes.values())
95 bvh_nodes_list.sort(key=lambda bvh_node: bvh_node.index)
96 return bvh_nodes_list
99 def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0):
100 # File loading stuff
101 # Open the file for importing
102 file = open(file_path, 'rU')
104 # Separate into a list of lists, each line a list of words.
105 file_lines = file.readlines()
106 # Non standard carrage returns?
107 if len(file_lines) == 1:
108 file_lines = file_lines[0].split('\r')
110 # Split by whitespace.
111 file_lines = [ll for ll in [l.split() for l in file_lines] if ll]
113 # Create hierarchy as empties
114 if file_lines[0][0].lower() == 'hierarchy':
115 # print 'Importing the BVH Hierarchy for:', file_path
116 pass
117 else:
118 raise Exception("This is not a BVH file")
120 bvh_nodes = {None: None}
121 bvh_nodes_serial = [None]
122 bvh_frame_count = None
123 bvh_frame_time = None
125 channelIndex = -1
127 lineIdx = 0 # An index for the file.
128 while lineIdx < len(file_lines) - 1:
129 if file_lines[lineIdx][0].lower() in {'root', 'joint'}:
131 # Join spaces into 1 word with underscores joining it.
132 if len(file_lines[lineIdx]) > 2:
133 file_lines[lineIdx][1] = '_'.join(file_lines[lineIdx][1:])
134 file_lines[lineIdx] = file_lines[lineIdx][:2]
136 # MAY NEED TO SUPPORT MULTIPLE ROOTS HERE! Still unsure weather multiple roots are possible?
138 # Make sure the names are unique - Object names will match joint names exactly and both will be unique.
139 name = file_lines[lineIdx][1]
141 # print '%snode: %s, parent: %s' % (len(bvh_nodes_serial) * ' ', name, bvh_nodes_serial[-1])
143 lineIdx += 2 # Increment to the next line (Offset)
144 rest_head_local = global_scale * Vector((
145 float(file_lines[lineIdx][1]),
146 float(file_lines[lineIdx][2]),
147 float(file_lines[lineIdx][3]),
149 lineIdx += 1 # Increment to the next line (Channels)
151 # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation]
152 # newChannel references indices to the motiondata,
153 # if not assigned then -1 refers to the last value that will be added on loading at a value of zero, this is appended
154 # We'll add a zero value onto the end of the MotionDATA so this always refers to a value.
155 my_channel = [-1, -1, -1, -1, -1, -1]
156 my_rot_order = [None, None, None]
157 rot_count = 0
158 for channel in file_lines[lineIdx][2:]:
159 channel = channel.lower()
160 channelIndex += 1 # So the index points to the right channel
161 if channel == 'xposition':
162 my_channel[0] = channelIndex
163 elif channel == 'yposition':
164 my_channel[1] = channelIndex
165 elif channel == 'zposition':
166 my_channel[2] = channelIndex
168 elif channel == 'xrotation':
169 my_channel[3] = channelIndex
170 my_rot_order[rot_count] = 0
171 rot_count += 1
172 elif channel == 'yrotation':
173 my_channel[4] = channelIndex
174 my_rot_order[rot_count] = 1
175 rot_count += 1
176 elif channel == 'zrotation':
177 my_channel[5] = channelIndex
178 my_rot_order[rot_count] = 2
179 rot_count += 1
181 channels = file_lines[lineIdx][2:]
183 my_parent = bvh_nodes_serial[-1] # account for none
185 # Apply the parents offset accumulatively
186 if my_parent is None:
187 rest_head_world = Vector(rest_head_local)
188 else:
189 rest_head_world = my_parent.rest_head_world + rest_head_local
191 bvh_node = bvh_nodes[name] = BVH_Node(
192 name,
193 rest_head_world,
194 rest_head_local,
195 my_parent,
196 my_channel,
197 my_rot_order,
198 len(bvh_nodes) - 1,
201 # If we have another child then we can call ourselves a parent, else
202 bvh_nodes_serial.append(bvh_node)
204 # Account for an end node.
205 # There is sometimes a name after 'End Site' but we will ignore it.
206 if file_lines[lineIdx][0].lower() == 'end' and file_lines[lineIdx][1].lower() == 'site':
207 # Increment to the next line (Offset)
208 lineIdx += 2
209 rest_tail = global_scale * Vector((
210 float(file_lines[lineIdx][1]),
211 float(file_lines[lineIdx][2]),
212 float(file_lines[lineIdx][3]),
215 bvh_nodes_serial[-1].rest_tail_world = bvh_nodes_serial[-1].rest_head_world + rest_tail
216 bvh_nodes_serial[-1].rest_tail_local = bvh_nodes_serial[-1].rest_head_local + rest_tail
218 # Just so we can remove the parents in a uniform way,
219 # the end has kids so this is a placeholder.
220 bvh_nodes_serial.append(None)
222 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0] == '}': # == ['}']
223 bvh_nodes_serial.pop() # Remove the last item
225 # End of the hierarchy. Begin the animation section of the file with
226 # the following header.
227 # MOTION
228 # Frames: n
229 # Frame Time: dt
230 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0].lower() == 'motion':
231 lineIdx += 1 # Read frame count.
232 if (
233 len(file_lines[lineIdx]) == 2 and
234 file_lines[lineIdx][0].lower() == 'frames:'
236 bvh_frame_count = int(file_lines[lineIdx][1])
238 lineIdx += 1 # Read frame rate.
239 if (
240 len(file_lines[lineIdx]) == 3 and
241 file_lines[lineIdx][0].lower() == 'frame' and
242 file_lines[lineIdx][1].lower() == 'time:'
244 bvh_frame_time = float(file_lines[lineIdx][2])
246 lineIdx += 1 # Set the cursor to the first frame
248 break
250 lineIdx += 1
252 # Remove the None value used for easy parent reference
253 del bvh_nodes[None]
254 # Don't use anymore
255 del bvh_nodes_serial
257 # importing world with any order but nicer to maintain order
258 # second life expects it, which isn't to spec.
259 bvh_nodes_list = sorted_nodes(bvh_nodes)
261 while lineIdx < len(file_lines):
262 line = file_lines[lineIdx]
263 for bvh_node in bvh_nodes_list:
264 # for bvh_node in bvh_nodes_serial:
265 lx = ly = lz = rx = ry = rz = 0.0
266 channels = bvh_node.channels
267 anim_data = bvh_node.anim_data
268 if channels[0] != -1:
269 lx = global_scale * float(line[channels[0]])
271 if channels[1] != -1:
272 ly = global_scale * float(line[channels[1]])
274 if channels[2] != -1:
275 lz = global_scale * float(line[channels[2]])
277 if channels[3] != -1 or channels[4] != -1 or channels[5] != -1:
279 rx = radians(float(line[channels[3]]))
280 ry = radians(float(line[channels[4]]))
281 rz = radians(float(line[channels[5]]))
283 # Done importing motion data #
284 anim_data.append((lx, ly, lz, rx, ry, rz))
285 lineIdx += 1
287 # Assign children
288 for bvh_node in bvh_nodes_list:
289 bvh_node_parent = bvh_node.parent
290 if bvh_node_parent:
291 bvh_node_parent.children.append(bvh_node)
293 # Now set the tip of each bvh_node
294 for bvh_node in bvh_nodes_list:
296 if not bvh_node.rest_tail_world:
297 if len(bvh_node.children) == 0:
298 # could just fail here, but rare BVH files have childless nodes
299 bvh_node.rest_tail_world = Vector(bvh_node.rest_head_world)
300 bvh_node.rest_tail_local = Vector(bvh_node.rest_head_local)
301 elif len(bvh_node.children) == 1:
302 bvh_node.rest_tail_world = Vector(bvh_node.children[0].rest_head_world)
303 bvh_node.rest_tail_local = bvh_node.rest_head_local + bvh_node.children[0].rest_head_local
304 else:
305 # allow this, see above
306 # if not bvh_node.children:
307 # raise Exception("bvh node has no end and no children. bad file")
309 # Removed temp for now
310 rest_tail_world = Vector((0.0, 0.0, 0.0))
311 rest_tail_local = Vector((0.0, 0.0, 0.0))
312 for bvh_node_child in bvh_node.children:
313 rest_tail_world += bvh_node_child.rest_head_world
314 rest_tail_local += bvh_node_child.rest_head_local
316 bvh_node.rest_tail_world = rest_tail_world * (1.0 / len(bvh_node.children))
317 bvh_node.rest_tail_local = rest_tail_local * (1.0 / len(bvh_node.children))
319 # Make sure tail isn't the same location as the head.
320 if (bvh_node.rest_tail_local - bvh_node.rest_head_local).length <= 0.001 * global_scale:
321 print("\tzero length node found:", bvh_node.name)
322 bvh_node.rest_tail_local.y = bvh_node.rest_tail_local.y + global_scale / 10
323 bvh_node.rest_tail_world.y = bvh_node.rest_tail_world.y + global_scale / 10
325 return bvh_nodes, bvh_frame_time, bvh_frame_count
328 def bvh_node_dict2objects(context, bvh_name, bvh_nodes, rotate_mode='NATIVE', frame_start=1, IMPORT_LOOP=False):
330 if frame_start < 1:
331 frame_start = 1
333 scene = context.scene
334 for obj in scene.objects:
335 obj.select_set(False)
337 objects = []
339 def add_ob(name):
340 obj = bpy.data.objects.new(name, None)
341 context.collection.objects.link(obj)
342 objects.append(obj)
343 obj.select_set(True)
345 # nicer drawing.
346 obj.empty_display_type = 'CUBE'
347 obj.empty_display_size = 0.1
349 return obj
351 # Add objects
352 for name, bvh_node in bvh_nodes.items():
353 bvh_node.temp = add_ob(name)
354 bvh_node.temp.rotation_mode = bvh_node.rot_order_str[::-1]
356 # Parent the objects
357 for bvh_node in bvh_nodes.values():
358 for bvh_node_child in bvh_node.children:
359 bvh_node_child.temp.parent = bvh_node.temp
361 # Offset
362 for bvh_node in bvh_nodes.values():
363 # Make relative to parents offset
364 bvh_node.temp.location = bvh_node.rest_head_local
366 # Add tail objects
367 for name, bvh_node in bvh_nodes.items():
368 if not bvh_node.children:
369 ob_end = add_ob(name + '_end')
370 ob_end.parent = bvh_node.temp
371 ob_end.location = bvh_node.rest_tail_world - bvh_node.rest_head_world
373 for name, bvh_node in bvh_nodes.items():
374 obj = bvh_node.temp
376 for frame_current in range(len(bvh_node.anim_data)):
378 lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current]
380 if bvh_node.has_loc:
381 obj.delta_location = Vector((lx, ly, lz)) - bvh_node.rest_head_world
382 obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current)
384 if bvh_node.has_rot:
385 obj.delta_rotation_euler = rx, ry, rz
386 obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current)
388 return objects
391 def bvh_node_dict2armature(
392 context,
393 bvh_name,
394 bvh_nodes,
395 bvh_frame_time,
396 rotate_mode='XYZ',
397 frame_start=1,
398 IMPORT_LOOP=False,
399 global_matrix=None,
400 use_fps_scale=False,
403 if frame_start < 1:
404 frame_start = 1
406 # Add the new armature,
407 scene = context.scene
408 for obj in scene.objects:
409 obj.select_set(False)
411 arm_data = bpy.data.armatures.new(bvh_name)
412 arm_ob = bpy.data.objects.new(bvh_name, arm_data)
414 context.collection.objects.link(arm_ob)
416 arm_ob.select_set(True)
417 context.view_layer.objects.active = arm_ob
419 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
420 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
422 bvh_nodes_list = sorted_nodes(bvh_nodes)
424 # Get the average bone length for zero length bones, we may not use this.
425 average_bone_length = 0.0
426 nonzero_count = 0
427 for bvh_node in bvh_nodes_list:
428 l = (bvh_node.rest_head_local - bvh_node.rest_tail_local).length
429 if l:
430 average_bone_length += l
431 nonzero_count += 1
433 # Very rare cases all bones could be zero length???
434 if not average_bone_length:
435 average_bone_length = 0.1
436 else:
437 # Normal operation
438 average_bone_length = average_bone_length / nonzero_count
440 # XXX, annoying, remove bone.
441 while arm_data.edit_bones:
442 arm_ob.edit_bones.remove(arm_data.edit_bones[-1])
444 ZERO_AREA_BONES = []
445 for bvh_node in bvh_nodes_list:
447 # New editbone
448 bone = bvh_node.temp = arm_data.edit_bones.new(bvh_node.name)
450 bone.head = bvh_node.rest_head_world
451 bone.tail = bvh_node.rest_tail_world
453 # Zero Length Bones! (an exceptional case)
454 if (bone.head - bone.tail).length < 0.001:
455 print("\tzero length bone found:", bone.name)
456 if bvh_node.parent:
457 ofs = bvh_node.parent.rest_head_local - bvh_node.parent.rest_tail_local
458 if ofs.length: # is our parent zero length also?? unlikely
459 bone.tail = bone.tail - ofs
460 else:
461 bone.tail.y = bone.tail.y + average_bone_length
462 else:
463 bone.tail.y = bone.tail.y + average_bone_length
465 ZERO_AREA_BONES.append(bone.name)
467 for bvh_node in bvh_nodes_list:
468 if bvh_node.parent:
469 # bvh_node.temp is the Editbone
471 # Set the bone parent
472 bvh_node.temp.parent = bvh_node.parent.temp
474 # Set the connection state
476 (not bvh_node.has_loc) and
477 (bvh_node.parent.temp.name not in ZERO_AREA_BONES) and
478 (bvh_node.parent.rest_tail_local == bvh_node.rest_head_local)
480 bvh_node.temp.use_connect = True
482 # Replace the editbone with the editbone name,
483 # to avoid memory errors accessing the editbone outside editmode
484 for bvh_node in bvh_nodes_list:
485 bvh_node.temp = bvh_node.temp.name
487 # Now Apply the animation to the armature
489 # Get armature animation data
490 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
492 pose = arm_ob.pose
493 pose_bones = pose.bones
495 if rotate_mode == 'NATIVE':
496 for bvh_node in bvh_nodes_list:
497 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
498 pose_bone = pose_bones[bone_name]
499 pose_bone.rotation_mode = bvh_node.rot_order_str
501 elif rotate_mode != 'QUATERNION':
502 for pose_bone in pose_bones:
503 pose_bone.rotation_mode = rotate_mode
504 else:
505 # Quats default
506 pass
508 context.view_layer.update()
510 arm_ob.animation_data_create()
511 action = bpy.data.actions.new(name=bvh_name)
512 arm_ob.animation_data.action = action
514 # Replace the bvh_node.temp (currently an editbone)
515 # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv)
516 num_frame = 0
517 for bvh_node in bvh_nodes_list:
518 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
519 pose_bone = pose_bones[bone_name]
520 rest_bone = arm_data.bones[bone_name]
521 bone_rest_matrix = rest_bone.matrix_local.to_3x3()
523 bone_rest_matrix_inv = Matrix(bone_rest_matrix)
524 bone_rest_matrix_inv.invert()
526 bone_rest_matrix_inv.resize_4x4()
527 bone_rest_matrix.resize_4x4()
528 bvh_node.temp = (pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv)
530 if 0 == num_frame:
531 num_frame = len(bvh_node.anim_data)
533 # Choose to skip some frames at the beginning. Frame 0 is the rest pose
534 # used internally by this importer. Frame 1, by convention, is also often
535 # the rest pose of the skeleton exported by the motion capture system.
536 skip_frame = 1
537 if num_frame > skip_frame:
538 num_frame = num_frame - skip_frame
540 # Create a shared time axis for all animation curves.
541 time = [float(frame_start)] * num_frame
542 if use_fps_scale:
543 dt = scene.render.fps * bvh_frame_time
544 for frame_i in range(1, num_frame):
545 time[frame_i] += float(frame_i) * dt
546 else:
547 for frame_i in range(1, num_frame):
548 time[frame_i] += float(frame_i)
550 # print("bvh_frame_time = %f, dt = %f, num_frame = %d"
551 # % (bvh_frame_time, dt, num_frame]))
553 for i, bvh_node in enumerate(bvh_nodes_list):
554 pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp
556 if bvh_node.has_loc:
557 # Not sure if there is a way to query this or access it in the
558 # PoseBone structure.
559 data_path = 'pose.bones["%s"].location' % pose_bone.name
561 location = [(0.0, 0.0, 0.0)] * num_frame
562 for frame_i in range(num_frame):
563 bvh_loc = bvh_node.anim_data[frame_i + skip_frame][:3]
565 bone_translate_matrix = Matrix.Translation(
566 Vector(bvh_loc) - bvh_node.rest_head_local)
567 location[frame_i] = (bone_rest_matrix_inv @
568 bone_translate_matrix).to_translation()
570 # For each location x, y, z.
571 for axis_i in range(3):
572 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
573 keyframe_points = curve.keyframe_points
574 keyframe_points.add(num_frame)
576 for frame_i in range(num_frame):
577 keyframe_points[frame_i].co = (
578 time[frame_i],
579 location[frame_i][axis_i],
582 if bvh_node.has_rot:
583 data_path = None
584 rotate = None
586 if 'QUATERNION' == rotate_mode:
587 rotate = [(1.0, 0.0, 0.0, 0.0)] * num_frame
588 data_path = ('pose.bones["%s"].rotation_quaternion'
589 % pose_bone.name)
590 else:
591 rotate = [(0.0, 0.0, 0.0)] * num_frame
592 data_path = ('pose.bones["%s"].rotation_euler' %
593 pose_bone.name)
595 prev_euler = Euler((0.0, 0.0, 0.0))
596 for frame_i in range(num_frame):
597 bvh_rot = bvh_node.anim_data[frame_i + skip_frame][3:]
599 # apply rotation order and convert to XYZ
600 # note that the rot_order_str is reversed.
601 euler = Euler(bvh_rot, bvh_node.rot_order_str[::-1])
602 bone_rotation_matrix = euler.to_matrix().to_4x4()
603 bone_rotation_matrix = (
604 bone_rest_matrix_inv @
605 bone_rotation_matrix @
606 bone_rest_matrix
609 if len(rotate[frame_i]) == 4:
610 rotate[frame_i] = bone_rotation_matrix.to_quaternion()
611 else:
612 rotate[frame_i] = bone_rotation_matrix.to_euler(
613 pose_bone.rotation_mode, prev_euler)
614 prev_euler = rotate[frame_i]
616 # For each euler angle x, y, z (or quaternion w, x, y, z).
617 for axis_i in range(len(rotate[0])):
618 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
619 keyframe_points = curve.keyframe_points
620 keyframe_points.add(num_frame)
622 for frame_i in range(num_frame):
623 keyframe_points[frame_i].co = (
624 time[frame_i],
625 rotate[frame_i][axis_i],
628 for cu in action.fcurves:
629 if IMPORT_LOOP:
630 pass # 2.5 doenst have cyclic now?
632 for bez in cu.keyframe_points:
633 bez.interpolation = 'LINEAR'
635 # finally apply matrix
636 arm_ob.matrix_world = global_matrix
637 bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
639 return arm_ob
642 def load(
643 context,
644 filepath,
646 target='ARMATURE',
647 rotate_mode='NATIVE',
648 global_scale=1.0,
649 use_cyclic=False,
650 frame_start=1,
651 global_matrix=None,
652 use_fps_scale=False,
653 update_scene_fps=False,
654 update_scene_duration=False,
655 report=print,
657 import time
658 t1 = time.time()
659 print("\tparsing bvh %r..." % filepath, end="")
661 bvh_nodes, bvh_frame_time, bvh_frame_count = read_bvh(
662 context, filepath,
663 rotate_mode=rotate_mode,
664 global_scale=global_scale,
667 print("%.4f" % (time.time() - t1))
669 scene = context.scene
670 frame_orig = scene.frame_current
672 # Broken BVH handling: guess frame rate when it is not contained in the file.
673 if bvh_frame_time is None:
674 report(
675 {'WARNING'},
676 "The BVH file does not contain frame duration in its MOTION "
677 "section, assuming the BVH and Blender scene have the same "
678 "frame rate"
680 bvh_frame_time = scene.render.fps_base / scene.render.fps
681 # No need to scale the frame rate, as they're equal now anyway.
682 use_fps_scale = False
684 if update_scene_fps:
685 _update_scene_fps(context, report, bvh_frame_time)
687 # Now that we have a 1-to-1 mapping of Blender frames and BVH frames, there is no need
688 # to scale the FPS any more. It's even better not to, to prevent roundoff errors.
689 use_fps_scale = False
691 if update_scene_duration:
692 _update_scene_duration(context, report, bvh_frame_count, bvh_frame_time, frame_start, use_fps_scale)
694 t1 = time.time()
695 print("\timporting to blender...", end="")
697 bvh_name = bpy.path.display_name_from_filepath(filepath)
699 if target == 'ARMATURE':
700 bvh_node_dict2armature(
701 context, bvh_name, bvh_nodes, bvh_frame_time,
702 rotate_mode=rotate_mode,
703 frame_start=frame_start,
704 IMPORT_LOOP=use_cyclic,
705 global_matrix=global_matrix,
706 use_fps_scale=use_fps_scale,
709 elif target == 'OBJECT':
710 bvh_node_dict2objects(
711 context, bvh_name, bvh_nodes,
712 rotate_mode=rotate_mode,
713 frame_start=frame_start,
714 IMPORT_LOOP=use_cyclic,
715 # global_matrix=global_matrix, # TODO
718 else:
719 report({'ERROR'}, "Invalid target %r (must be 'ARMATURE' or 'OBJECT')" % target)
720 return {'CANCELLED'}
722 print('Done in %.4f\n' % (time.time() - t1))
724 context.scene.frame_set(frame_orig)
726 return {'FINISHED'}
729 def _update_scene_fps(context, report, bvh_frame_time):
730 """Update the scene's FPS settings from the BVH, but only if the BVH contains enough info."""
732 # Broken BVH handling: prevent division by zero.
733 if bvh_frame_time == 0.0:
734 report(
735 {'WARNING'},
736 "Unable to update scene frame rate, as the BVH file "
737 "contains a zero frame duration in its MOTION section",
739 return
741 scene = context.scene
742 scene_fps = scene.render.fps / scene.render.fps_base
743 new_fps = 1.0 / bvh_frame_time
745 if scene.render.fps != new_fps or scene.render.fps_base != 1.0:
746 print("\tupdating scene FPS (was %f) to BVH FPS (%f)" % (scene_fps, new_fps))
747 scene.render.fps = new_fps
748 scene.render.fps_base = 1.0
751 def _update_scene_duration(
752 context, report, bvh_frame_count, bvh_frame_time, frame_start,
753 use_fps_scale):
754 """Extend the scene's duration so that the BVH file fits in its entirety."""
756 if bvh_frame_count is None:
757 report(
758 {'WARNING'},
759 "Unable to extend the scene duration, as the BVH file does not "
760 "contain the number of frames in its MOTION section",
762 return
764 # Not likely, but it can happen when a BVH is just used to store an armature.
765 if bvh_frame_count == 0:
766 return
768 if use_fps_scale:
769 scene_fps = context.scene.render.fps / context.scene.render.fps_base
770 scaled_frame_count = int(ceil(bvh_frame_count * bvh_frame_time * scene_fps))
771 bvh_last_frame = frame_start + scaled_frame_count
772 else:
773 bvh_last_frame = frame_start + bvh_frame_count
775 # Only extend the scene, never shorten it.
776 if context.scene.frame_end < bvh_last_frame:
777 context.scene.frame_end = bvh_last_frame