io_mesh_uv_layout: speed up png export with OIIO (x7)
[blender-addons.git] / io_anim_bvh / import_bvh.py
blobc02ea9548a661f37bf9124463267b1f37f1c92b0
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 bpy.app.translations import pgettext_tip as tip_
9 from mathutils import Vector, Euler, Matrix
12 class BVH_Node:
13 __slots__ = (
14 # Bvh joint name.
15 'name',
16 # BVH_Node type or None for no parent.
17 'parent',
18 # A list of children of this type..
19 'children',
20 # Worldspace rest location for the head of this node.
21 'rest_head_world',
22 # Localspace rest location for the head of this node.
23 'rest_head_local',
24 # Worldspace rest location for the tail of this node.
25 'rest_tail_world',
26 # Worldspace rest location for the tail of this node.
27 'rest_tail_local',
28 # List of 6 ints, -1 for an unused channel,
29 # otherwise an index for the BVH motion data lines,
30 # loc triple then rot triple.
31 'channels',
32 # A triple of indices as to the order rotation is applied.
33 # [0,1,2] is x/y/z - [None, None, None] if no rotation..
34 'rot_order',
35 # Same as above but a string 'XYZ' format..
36 'rot_order_str',
37 # A list one tuple's one for each frame: (locx, locy, locz, rotx, roty, rotz),
38 # euler rotation ALWAYS stored xyz order, even when native used.
39 'anim_data',
40 # Convenience function, bool, same as: (channels[0] != -1 or channels[1] != -1 or channels[2] != -1).
41 'has_loc',
42 # Convenience function, bool, same as: (channels[3] != -1 or channels[4] != -1 or channels[5] != -1).
43 'has_rot',
44 # Index from the file, not strictly needed but nice to maintain order.
45 'index',
46 # Use this for whatever you want.
47 'temp',
50 _eul_order_lookup = {
51 (None, None, None): 'XYZ', # XXX Dummy one, no rotation anyway!
52 (0, 1, 2): 'XYZ',
53 (0, 2, 1): 'XZY',
54 (1, 0, 2): 'YXZ',
55 (1, 2, 0): 'YZX',
56 (2, 0, 1): 'ZXY',
57 (2, 1, 0): 'ZYX',
60 def __init__(self, name, rest_head_world, rest_head_local, parent, channels, rot_order, index):
61 self.name = name
62 self.rest_head_world = rest_head_world
63 self.rest_head_local = rest_head_local
64 self.rest_tail_world = None
65 self.rest_tail_local = None
66 self.parent = parent
67 self.channels = channels
68 self.rot_order = tuple(rot_order)
69 self.rot_order_str = BVH_Node._eul_order_lookup[self.rot_order]
70 self.index = index
72 # convenience functions
73 self.has_loc = channels[0] != -1 or channels[1] != -1 or channels[2] != -1
74 self.has_rot = channels[3] != -1 or channels[4] != -1 or channels[5] != -1
76 self.children = []
78 # List of 6 length tuples: (lx, ly, lz, rx, ry, rz)
79 # even if the channels aren't used they will just be zero.
80 self.anim_data = [(0, 0, 0, 0, 0, 0)]
82 def __repr__(self):
83 return (
84 "BVH name: '%s', rest_loc:(%.3f,%.3f,%.3f), rest_tail:(%.3f,%.3f,%.3f)" % (
85 self.name,
86 *self.rest_head_world,
87 *self.rest_head_world,
92 def sorted_nodes(bvh_nodes):
93 bvh_nodes_list = list(bvh_nodes.values())
94 bvh_nodes_list.sort(key=lambda bvh_node: bvh_node.index)
95 return bvh_nodes_list
98 def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0):
99 # File loading stuff
100 # Open the file for importing
101 file = open(file_path, 'r')
103 # Separate into a list of lists, each line a list of words.
104 file_lines = file.readlines()
105 # Non standard carriage returns?
106 if len(file_lines) == 1:
107 file_lines = file_lines[0].split('\r')
109 # Split by whitespace.
110 file_lines = [ll for ll in [l.split() for l in file_lines] if ll]
112 # Create hierarchy as empties
113 if file_lines[0][0].lower() == 'hierarchy':
114 # print 'Importing the BVH Hierarchy for:', file_path
115 pass
116 else:
117 raise Exception("This is not a BVH file")
119 bvh_nodes = {None: None}
120 bvh_nodes_serial = [None]
121 bvh_frame_count = None
122 bvh_frame_time = None
124 channelIndex = -1
126 lineIdx = 0 # An index for the file.
127 while lineIdx < len(file_lines) - 1:
128 if file_lines[lineIdx][0].lower() in {'root', 'joint'}:
130 # Join spaces into 1 word with underscores joining it.
131 if len(file_lines[lineIdx]) > 2:
132 file_lines[lineIdx][1] = '_'.join(file_lines[lineIdx][1:])
133 file_lines[lineIdx] = file_lines[lineIdx][:2]
135 # MAY NEED TO SUPPORT MULTIPLE ROOTS HERE! Still unsure weather multiple roots are possible?
137 # Make sure the names are unique - Object names will match joint names exactly and both will be unique.
138 name = file_lines[lineIdx][1]
140 # print '%snode: %s, parent: %s' % (len(bvh_nodes_serial) * ' ', name, bvh_nodes_serial[-1])
142 lineIdx += 2 # Increment to the next line (Offset)
143 rest_head_local = global_scale * Vector((
144 float(file_lines[lineIdx][1]),
145 float(file_lines[lineIdx][2]),
146 float(file_lines[lineIdx][3]),
148 lineIdx += 1 # Increment to the next line (Channels)
150 # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation]
151 # newChannel references indices to the motiondata,
152 # if not assigned then -1 refers to the last value that will be added on loading at a value of zero, this is appended
153 # We'll add a zero value onto the end of the MotionDATA so this always refers to a value.
154 my_channel = [-1, -1, -1, -1, -1, -1]
155 my_rot_order = [None, None, None]
156 rot_count = 0
157 for channel in file_lines[lineIdx][2:]:
158 channel = channel.lower()
159 channelIndex += 1 # So the index points to the right channel
160 if channel == 'xposition':
161 my_channel[0] = channelIndex
162 elif channel == 'yposition':
163 my_channel[1] = channelIndex
164 elif channel == 'zposition':
165 my_channel[2] = channelIndex
167 elif channel == 'xrotation':
168 my_channel[3] = channelIndex
169 my_rot_order[rot_count] = 0
170 rot_count += 1
171 elif channel == 'yrotation':
172 my_channel[4] = channelIndex
173 my_rot_order[rot_count] = 1
174 rot_count += 1
175 elif channel == 'zrotation':
176 my_channel[5] = channelIndex
177 my_rot_order[rot_count] = 2
178 rot_count += 1
180 channels = file_lines[lineIdx][2:]
182 my_parent = bvh_nodes_serial[-1] # account for none
184 # Apply the parents offset accumulatively
185 if my_parent is None:
186 rest_head_world = Vector(rest_head_local)
187 else:
188 rest_head_world = my_parent.rest_head_world + rest_head_local
190 bvh_node = bvh_nodes[name] = BVH_Node(
191 name,
192 rest_head_world,
193 rest_head_local,
194 my_parent,
195 my_channel,
196 my_rot_order,
197 len(bvh_nodes) - 1,
200 # If we have another child then we can call ourselves a parent, else
201 bvh_nodes_serial.append(bvh_node)
203 # Account for an end node.
204 # There is sometimes a name after 'End Site' but we will ignore it.
205 if file_lines[lineIdx][0].lower() == 'end' and file_lines[lineIdx][1].lower() == 'site':
206 # Increment to the next line (Offset)
207 lineIdx += 2
208 rest_tail = global_scale * Vector((
209 float(file_lines[lineIdx][1]),
210 float(file_lines[lineIdx][2]),
211 float(file_lines[lineIdx][3]),
214 bvh_nodes_serial[-1].rest_tail_world = bvh_nodes_serial[-1].rest_head_world + rest_tail
215 bvh_nodes_serial[-1].rest_tail_local = bvh_nodes_serial[-1].rest_head_local + rest_tail
217 # Just so we can remove the parents in a uniform way,
218 # the end has kids so this is a placeholder.
219 bvh_nodes_serial.append(None)
221 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0] == '}': # == ['}']
222 bvh_nodes_serial.pop() # Remove the last item
224 # End of the hierarchy. Begin the animation section of the file with
225 # the following header.
226 # MOTION
227 # Frames: n
228 # Frame Time: dt
229 if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0].lower() == 'motion':
230 lineIdx += 1 # Read frame count.
231 if (
232 len(file_lines[lineIdx]) == 2 and
233 file_lines[lineIdx][0].lower() == 'frames:'
235 bvh_frame_count = int(file_lines[lineIdx][1])
237 lineIdx += 1 # Read frame rate.
238 if (
239 len(file_lines[lineIdx]) == 3 and
240 file_lines[lineIdx][0].lower() == 'frame' and
241 file_lines[lineIdx][1].lower() == 'time:'
243 bvh_frame_time = float(file_lines[lineIdx][2])
245 lineIdx += 1 # Set the cursor to the first frame
247 break
249 lineIdx += 1
251 # Remove the None value used for easy parent reference
252 del bvh_nodes[None]
253 # Don't use anymore
254 del bvh_nodes_serial
256 # importing world with any order but nicer to maintain order
257 # second life expects it, which isn't to spec.
258 bvh_nodes_list = sorted_nodes(bvh_nodes)
260 while lineIdx < len(file_lines):
261 line = file_lines[lineIdx]
262 for bvh_node in bvh_nodes_list:
263 # for bvh_node in bvh_nodes_serial:
264 lx = ly = lz = rx = ry = rz = 0.0
265 channels = bvh_node.channels
266 anim_data = bvh_node.anim_data
267 if channels[0] != -1:
268 lx = global_scale * float(line[channels[0]])
270 if channels[1] != -1:
271 ly = global_scale * float(line[channels[1]])
273 if channels[2] != -1:
274 lz = global_scale * float(line[channels[2]])
276 if channels[3] != -1 or channels[4] != -1 or channels[5] != -1:
278 rx = radians(float(line[channels[3]]))
279 ry = radians(float(line[channels[4]]))
280 rz = radians(float(line[channels[5]]))
282 # Done importing motion data #
283 anim_data.append((lx, ly, lz, rx, ry, rz))
284 lineIdx += 1
286 # Assign children
287 for bvh_node in bvh_nodes_list:
288 bvh_node_parent = bvh_node.parent
289 if bvh_node_parent:
290 bvh_node_parent.children.append(bvh_node)
292 # Now set the tip of each bvh_node
293 for bvh_node in bvh_nodes_list:
295 if not bvh_node.rest_tail_world:
296 if len(bvh_node.children) == 0:
297 # could just fail here, but rare BVH files have childless nodes
298 bvh_node.rest_tail_world = Vector(bvh_node.rest_head_world)
299 bvh_node.rest_tail_local = Vector(bvh_node.rest_head_local)
300 elif len(bvh_node.children) == 1:
301 bvh_node.rest_tail_world = Vector(bvh_node.children[0].rest_head_world)
302 bvh_node.rest_tail_local = bvh_node.rest_head_local + bvh_node.children[0].rest_head_local
303 else:
304 # allow this, see above
305 # if not bvh_node.children:
306 # raise Exception("bvh node has no end and no children. bad file")
308 # Removed temp for now
309 rest_tail_world = Vector((0.0, 0.0, 0.0))
310 rest_tail_local = Vector((0.0, 0.0, 0.0))
311 for bvh_node_child in bvh_node.children:
312 rest_tail_world += bvh_node_child.rest_head_world
313 rest_tail_local += bvh_node_child.rest_head_local
315 bvh_node.rest_tail_world = rest_tail_world * (1.0 / len(bvh_node.children))
316 bvh_node.rest_tail_local = rest_tail_local * (1.0 / len(bvh_node.children))
318 # Make sure tail isn't the same location as the head.
319 if (bvh_node.rest_tail_local - bvh_node.rest_head_local).length <= 0.001 * global_scale:
320 print("\tzero length node found:", bvh_node.name)
321 bvh_node.rest_tail_local.y = bvh_node.rest_tail_local.y + global_scale / 10
322 bvh_node.rest_tail_world.y = bvh_node.rest_tail_world.y + global_scale / 10
324 return bvh_nodes, bvh_frame_time, bvh_frame_count
327 def bvh_node_dict2objects(context, bvh_name, bvh_nodes, rotate_mode='NATIVE', frame_start=1, IMPORT_LOOP=False):
329 if frame_start < 1:
330 frame_start = 1
332 scene = context.scene
333 for obj in scene.objects:
334 obj.select_set(False)
336 objects = []
338 def add_ob(name):
339 obj = bpy.data.objects.new(name, None)
340 context.collection.objects.link(obj)
341 objects.append(obj)
342 obj.select_set(True)
344 # nicer drawing.
345 obj.empty_display_type = 'CUBE'
346 obj.empty_display_size = 0.1
348 return obj
350 # Add objects
351 for name, bvh_node in bvh_nodes.items():
352 bvh_node.temp = add_ob(name)
353 bvh_node.temp.rotation_mode = bvh_node.rot_order_str[::-1]
355 # Parent the objects
356 for bvh_node in bvh_nodes.values():
357 for bvh_node_child in bvh_node.children:
358 bvh_node_child.temp.parent = bvh_node.temp
360 # Offset
361 for bvh_node in bvh_nodes.values():
362 # Make relative to parents offset
363 bvh_node.temp.location = bvh_node.rest_head_local
365 # Add tail objects
366 for name, bvh_node in bvh_nodes.items():
367 if not bvh_node.children:
368 ob_end = add_ob(name + '_end')
369 ob_end.parent = bvh_node.temp
370 ob_end.location = bvh_node.rest_tail_world - bvh_node.rest_head_world
372 for name, bvh_node in bvh_nodes.items():
373 obj = bvh_node.temp
375 for frame_current in range(len(bvh_node.anim_data)):
377 lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current]
379 if bvh_node.has_loc:
380 obj.delta_location = Vector((lx, ly, lz)) - bvh_node.rest_head_world
381 obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current)
383 if bvh_node.has_rot:
384 obj.delta_rotation_euler = rx, ry, rz
385 obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current)
387 return objects
390 def bvh_node_dict2armature(
391 context,
392 bvh_name,
393 bvh_nodes,
394 bvh_frame_time,
395 rotate_mode='XYZ',
396 frame_start=1,
397 IMPORT_LOOP=False,
398 global_matrix=None,
399 use_fps_scale=False,
402 if frame_start < 1:
403 frame_start = 1
405 # Add the new armature,
406 scene = context.scene
407 for obj in scene.objects:
408 obj.select_set(False)
410 arm_data = bpy.data.armatures.new(bvh_name)
411 arm_ob = bpy.data.objects.new(bvh_name, arm_data)
413 context.collection.objects.link(arm_ob)
415 arm_ob.select_set(True)
416 context.view_layer.objects.active = arm_ob
418 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
419 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
421 bvh_nodes_list = sorted_nodes(bvh_nodes)
423 # Get the average bone length for zero length bones, we may not use this.
424 average_bone_length = 0.0
425 nonzero_count = 0
426 for bvh_node in bvh_nodes_list:
427 l = (bvh_node.rest_head_local - bvh_node.rest_tail_local).length
428 if l:
429 average_bone_length += l
430 nonzero_count += 1
432 # Very rare cases all bones could be zero length???
433 if not average_bone_length:
434 average_bone_length = 0.1
435 else:
436 # Normal operation
437 average_bone_length = average_bone_length / nonzero_count
439 # XXX, annoying, remove bone.
440 while arm_data.edit_bones:
441 arm_ob.edit_bones.remove(arm_data.edit_bones[-1])
443 ZERO_AREA_BONES = []
444 for bvh_node in bvh_nodes_list:
446 # New editbone
447 bone = bvh_node.temp = arm_data.edit_bones.new(bvh_node.name)
449 bone.head = bvh_node.rest_head_world
450 bone.tail = bvh_node.rest_tail_world
452 # Zero Length Bones! (an exceptional case)
453 if (bone.head - bone.tail).length < 0.001:
454 print("\tzero length bone found:", bone.name)
455 if bvh_node.parent:
456 ofs = bvh_node.parent.rest_head_local - bvh_node.parent.rest_tail_local
457 if ofs.length: # is our parent zero length also?? unlikely
458 bone.tail = bone.tail - ofs
459 else:
460 bone.tail.y = bone.tail.y + average_bone_length
461 else:
462 bone.tail.y = bone.tail.y + average_bone_length
464 ZERO_AREA_BONES.append(bone.name)
466 for bvh_node in bvh_nodes_list:
467 if bvh_node.parent:
468 # bvh_node.temp is the Editbone
470 # Set the bone parent
471 bvh_node.temp.parent = bvh_node.parent.temp
473 # Set the connection state
475 (not bvh_node.has_loc) and
476 (bvh_node.parent.temp.name not in ZERO_AREA_BONES) and
477 (bvh_node.parent.rest_tail_local == bvh_node.rest_head_local)
479 bvh_node.temp.use_connect = True
481 # Replace the editbone with the editbone name,
482 # to avoid memory errors accessing the editbone outside editmode
483 for bvh_node in bvh_nodes_list:
484 bvh_node.temp = bvh_node.temp.name
486 # Now Apply the animation to the armature
488 # Get armature animation data
489 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
491 pose = arm_ob.pose
492 pose_bones = pose.bones
494 if rotate_mode == 'NATIVE':
495 for bvh_node in bvh_nodes_list:
496 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
497 pose_bone = pose_bones[bone_name]
498 pose_bone.rotation_mode = bvh_node.rot_order_str
500 elif rotate_mode != 'QUATERNION':
501 for pose_bone in pose_bones:
502 pose_bone.rotation_mode = rotate_mode
503 else:
504 # Quats default
505 pass
507 context.view_layer.update()
509 arm_ob.animation_data_create()
510 action = bpy.data.actions.new(name=bvh_name)
511 arm_ob.animation_data.action = action
513 # Replace the bvh_node.temp (currently an editbone)
514 # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv)
515 num_frame = 0
516 for bvh_node in bvh_nodes_list:
517 bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened.
518 pose_bone = pose_bones[bone_name]
519 rest_bone = arm_data.bones[bone_name]
520 bone_rest_matrix = rest_bone.matrix_local.to_3x3()
522 bone_rest_matrix_inv = Matrix(bone_rest_matrix)
523 bone_rest_matrix_inv.invert()
525 bone_rest_matrix_inv.resize_4x4()
526 bone_rest_matrix.resize_4x4()
527 bvh_node.temp = (pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv)
529 if 0 == num_frame:
530 num_frame = len(bvh_node.anim_data)
532 # Choose to skip some frames at the beginning. Frame 0 is the rest pose
533 # used internally by this importer. Frame 1, by convention, is also often
534 # the rest pose of the skeleton exported by the motion capture system.
535 skip_frame = 1
536 if num_frame > skip_frame:
537 num_frame = num_frame - skip_frame
539 # Create a shared time axis for all animation curves.
540 time = [float(frame_start)] * num_frame
541 if use_fps_scale:
542 dt = scene.render.fps * bvh_frame_time
543 for frame_i in range(1, num_frame):
544 time[frame_i] += float(frame_i) * dt
545 else:
546 for frame_i in range(1, num_frame):
547 time[frame_i] += float(frame_i)
549 # print("bvh_frame_time = %f, dt = %f, num_frame = %d"
550 # % (bvh_frame_time, dt, num_frame]))
552 for i, bvh_node in enumerate(bvh_nodes_list):
553 pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp
555 if bvh_node.has_loc:
556 # Not sure if there is a way to query this or access it in the
557 # PoseBone structure.
558 data_path = 'pose.bones["%s"].location' % pose_bone.name
560 location = [(0.0, 0.0, 0.0)] * num_frame
561 for frame_i in range(num_frame):
562 bvh_loc = bvh_node.anim_data[frame_i + skip_frame][:3]
564 bone_translate_matrix = Matrix.Translation(
565 Vector(bvh_loc) - bvh_node.rest_head_local)
566 location[frame_i] = (bone_rest_matrix_inv @
567 bone_translate_matrix).to_translation()
569 # For each location x, y, z.
570 for axis_i in range(3):
571 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
572 keyframe_points = curve.keyframe_points
573 keyframe_points.add(num_frame)
575 for frame_i in range(num_frame):
576 keyframe_points[frame_i].co = (
577 time[frame_i],
578 location[frame_i][axis_i],
581 if bvh_node.has_rot:
582 data_path = None
583 rotate = None
585 if 'QUATERNION' == rotate_mode:
586 rotate = [(1.0, 0.0, 0.0, 0.0)] * num_frame
587 data_path = ('pose.bones["%s"].rotation_quaternion'
588 % pose_bone.name)
589 else:
590 rotate = [(0.0, 0.0, 0.0)] * num_frame
591 data_path = ('pose.bones["%s"].rotation_euler' %
592 pose_bone.name)
594 prev_euler = Euler((0.0, 0.0, 0.0))
595 for frame_i in range(num_frame):
596 bvh_rot = bvh_node.anim_data[frame_i + skip_frame][3:]
598 # apply rotation order and convert to XYZ
599 # note that the rot_order_str is reversed.
600 euler = Euler(bvh_rot, bvh_node.rot_order_str[::-1])
601 bone_rotation_matrix = euler.to_matrix().to_4x4()
602 bone_rotation_matrix = (
603 bone_rest_matrix_inv @
604 bone_rotation_matrix @
605 bone_rest_matrix
608 if len(rotate[frame_i]) == 4:
609 rotate[frame_i] = bone_rotation_matrix.to_quaternion()
610 else:
611 rotate[frame_i] = bone_rotation_matrix.to_euler(
612 pose_bone.rotation_mode, prev_euler)
613 prev_euler = rotate[frame_i]
615 # For each euler angle x, y, z (or quaternion w, x, y, z).
616 for axis_i in range(len(rotate[0])):
617 curve = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bvh_node.name)
618 keyframe_points = curve.keyframe_points
619 keyframe_points.add(num_frame)
621 for frame_i in range(num_frame):
622 keyframe_points[frame_i].co = (
623 time[frame_i],
624 rotate[frame_i][axis_i],
627 for cu in action.fcurves:
628 if IMPORT_LOOP:
629 pass # 2.5 doenst have cyclic now?
631 for bez in cu.keyframe_points:
632 bez.interpolation = 'LINEAR'
634 # finally apply matrix
635 arm_ob.matrix_world = global_matrix
636 bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
638 return arm_ob
641 def load(
642 context,
643 filepath,
645 target='ARMATURE',
646 rotate_mode='NATIVE',
647 global_scale=1.0,
648 use_cyclic=False,
649 frame_start=1,
650 global_matrix=None,
651 use_fps_scale=False,
652 update_scene_fps=False,
653 update_scene_duration=False,
654 report=print,
656 import time
657 t1 = time.time()
658 print("\tparsing bvh %r..." % filepath, end="")
660 bvh_nodes, bvh_frame_time, bvh_frame_count = read_bvh(
661 context, filepath,
662 rotate_mode=rotate_mode,
663 global_scale=global_scale,
666 print("%.4f" % (time.time() - t1))
668 scene = context.scene
669 frame_orig = scene.frame_current
671 # Broken BVH handling: guess frame rate when it is not contained in the file.
672 if bvh_frame_time is None:
673 report(
674 {'WARNING'},
675 "The BVH file does not contain frame duration in its MOTION "
676 "section, assuming the BVH and Blender scene have the same "
677 "frame rate"
679 bvh_frame_time = scene.render.fps_base / scene.render.fps
680 # No need to scale the frame rate, as they're equal now anyway.
681 use_fps_scale = False
683 if update_scene_fps:
684 _update_scene_fps(context, report, bvh_frame_time)
686 # Now that we have a 1-to-1 mapping of Blender frames and BVH frames, there is no need
687 # to scale the FPS any more. It's even better not to, to prevent roundoff errors.
688 use_fps_scale = False
690 if update_scene_duration:
691 _update_scene_duration(context, report, bvh_frame_count, bvh_frame_time, frame_start, use_fps_scale)
693 t1 = time.time()
694 print("\timporting to blender...", end="")
696 bvh_name = bpy.path.display_name_from_filepath(filepath)
698 if target == 'ARMATURE':
699 bvh_node_dict2armature(
700 context, bvh_name, bvh_nodes, bvh_frame_time,
701 rotate_mode=rotate_mode,
702 frame_start=frame_start,
703 IMPORT_LOOP=use_cyclic,
704 global_matrix=global_matrix,
705 use_fps_scale=use_fps_scale,
708 elif target == 'OBJECT':
709 bvh_node_dict2objects(
710 context, bvh_name, bvh_nodes,
711 rotate_mode=rotate_mode,
712 frame_start=frame_start,
713 IMPORT_LOOP=use_cyclic,
714 # global_matrix=global_matrix, # TODO
717 else:
718 report({'ERROR'}, tip_("Invalid target %r (must be 'ARMATURE' or 'OBJECT')") % target)
719 return {'CANCELLED'}
721 print('Done in %.4f\n' % (time.time() - t1))
723 context.scene.frame_set(frame_orig)
725 return {'FINISHED'}
728 def _update_scene_fps(context, report, bvh_frame_time):
729 """Update the scene's FPS settings from the BVH, but only if the BVH contains enough info."""
731 # Broken BVH handling: prevent division by zero.
732 if bvh_frame_time == 0.0:
733 report(
734 {'WARNING'},
735 "Unable to update scene frame rate, as the BVH file "
736 "contains a zero frame duration in its MOTION section",
738 return
740 scene = context.scene
741 scene_fps = scene.render.fps / scene.render.fps_base
742 new_fps = 1.0 / bvh_frame_time
744 if scene.render.fps != new_fps or scene.render.fps_base != 1.0:
745 print("\tupdating scene FPS (was %f) to BVH FPS (%f)" % (scene_fps, new_fps))
746 scene.render.fps = int(round(new_fps))
747 scene.render.fps_base = scene.render.fps / new_fps
750 def _update_scene_duration(
751 context, report, bvh_frame_count, bvh_frame_time, frame_start,
752 use_fps_scale):
753 """Extend the scene's duration so that the BVH file fits in its entirety."""
755 if bvh_frame_count is None:
756 report(
757 {'WARNING'},
758 "Unable to extend the scene duration, as the BVH file does not "
759 "contain the number of frames in its MOTION section",
761 return
763 # Not likely, but it can happen when a BVH is just used to store an armature.
764 if bvh_frame_count == 0:
765 return
767 if use_fps_scale:
768 scene_fps = context.scene.render.fps / context.scene.render.fps_base
769 scaled_frame_count = int(ceil(bvh_frame_count * bvh_frame_time * scene_fps))
770 bvh_last_frame = frame_start + scaled_frame_count
771 else:
772 bvh_last_frame = frame_start + bvh_frame_count
774 # Only extend the scene, never shorten it.
775 if context.scene.frame_end < bvh_last_frame:
776 context.scene.frame_end = bvh_last_frame