I was having trouble, error when “stop recording”. Also the model was getting flipped in recording (not in streaming). I fixed it with the patch below but much of it was “AI” and so can’t trust it fully. But it works for me. Hope this is helpful to you.
--- rebocap_blender_plugin/ops/rebocap_connection.py 2025-11-05 15:25:10.000000000 -0800
+++ /rebocap_blender_plugin/ops/rebocap_connection.py 2026-01-10 14:35:16.139730400 -0800
@@ -10,6 +10,44 @@
from .utils import show_message_box
from ..rebocap_api.rebocap_ws_sdk import REBOCAP_JOINT_NAMES
+def ensure_fcurve(action, datablock, data_path: str, index: int):
+ """
+ Blender 5.0+: Action.fcurves was removed; use fcurve_ensure_for_datablock().
+ Older Blender: fall back to action.fcurves.new().
+ """
+ if hasattr(action, "fcurve_ensure_for_datablock"):
+ return action.fcurve_ensure_for_datablock(
+ datablock=datablock,
+ data_path=data_path,
+ index=index,
+ )
+ # Blender <= 4.x fallback
+ return action.fcurves.new(data_path=data_path, index=index)
+
+def quat_make_continuous(prev_q: Optional[Quaternion], q: Quaternion) -> Quaternion:
+ """
+ Ensure quaternion continuity across frames (avoid q / -q sign flips which
+ cause long-arc interpolation and apparent skeleton flips when baking).
+ """
+ q = q.copy()
+ q.normalize()
+ if prev_q is not None and prev_q.dot(q) < 0.0:
+ q = -q
+ return q
+
+def set_kp_linear_vector(kp):
+ # Linear interpolation prevents Bezier overshoot between quaternion keys
+ kp.interpolation = 'LINEAR'
+ # VECTOR handles (optional) make it strictly piecewise-linear and avoid auto handle weirdness
+ kp.handle_left_type = 'VECTOR'
+ kp.handle_right_type = 'VECTOR'
+
+def set_curve_quat_safe(curve):
+ # Depending on Blender version, these exist; safe to guard
+ if hasattr(curve, "auto_smoothing"):
+ curve.auto_smoothing = 'NONE'
+ if hasattr(curve, "extrapolation"):
+ curve.extrapolation = 'CONSTANT'
class RebocapWsSdkV1(RebocapWsSdk):
@@ -410,7 +448,6 @@
if right_shoulder_bone is None and right_arm_bone is not None:
all_pose[17] = all_pose[14] @ all_pose[17]
- # print(all_pose[1])
for idx, bone in enumerate(map_bones):
if bone is None or idx == -1 or idx in (10, 11, 22, 23):
if idx == 0: # hip must bind!!!
@@ -479,6 +516,11 @@
if source_obj is None:
return
+
+ # ensure all driven bones are in quaternion mode before baking
+ for pb in source_obj.pose.bones:
+ pb.rotation_mode = 'QUATERNION'
+
all_pose_list = []
for record in record_list:
all_pose_list.append([Quaternion([pose[3], pose[0], pose[1], pose[2]]) for pose in record[1]])
@@ -522,31 +564,44 @@
if idx == 0: # hip must bind!!!
break
continue
+
bone_name = bone.name
bone_info = obj_info.get_or_insert_bone(bone_name)
rest_matrix_to_world, rest_matrix_from_world = bone_info.get_rest_init()
+
rotation_quaternion = []
t = []
+
+ prev_q = None
+ begin_t = record_list[0][3]
for fn, frame in enumerate(record_list):
t.append((frame[3] - begin_t) / dt + 1)
+
pose = all_pose_list[fn][idx]
new_pose = rest_matrix_from_world @ pose @ rest_matrix_to_world
+ new_pose = quat_make_continuous(prev_q, new_pose)
rotation_quaternion.append(new_pose)
+ prev_q = new_pose
+
data_path = 'pose.bones["%s"].rotation_quaternion' % bone.name
for axis_i in range(4):
- curve = action.fcurves.new(data_path=data_path, index=axis_i)
+ curve = ensure_fcurve(action, obj, data_path, axis_i)
+ set_curve_quat_safe(curve)
keyframe_points = curve.keyframe_points
frame_count = len(rotation_quaternion)
keyframe_points.add(frame_count)
- for i, rotation_quaternion_item in enumerate(rotation_quaternion):
- keyframe_points[i].co = (
- t[i],
- rotation_quaternion_item[axis_i]
- )
+ for i, q in enumerate(rotation_quaternion):
+ kp = keyframe_points[i]
+ kp.co = (t[i], q[axis_i])
+ kp.interpolation = 'LINEAR'
+ kp.handle_left_type = 'VECTOR'
+ kp.handle_right_type = 'VECTOR'
+ curve.update()
+ # KEY ROOT LOCATION ON THE ACTUAL ROOT BONE (matches streaming behavior)
if idx == 0 and obj_info.root_bone is not None:
- bone = obj_info.root_bone
- data_path = 'pose.bones["%s"].location' % bone.name
+ root_pb = obj_info.root_bone
+ data_path = f'pose.bones["{root_pb.name}"].location'
all_trans_new = []
for i, frame in enumerate(record_list):
@@ -559,16 +614,18 @@
all_trans_new.append(new_local_matrix.translation)
for axis_i in range(3):
- curve = action.fcurves.new(data_path=data_path, index=axis_i)
+ curve = ensure_fcurve(action, obj, data_path, axis_i)
+ set_curve_quat_safe(curve)
keyframe_points = curve.keyframe_points
frame_count = len(record_list)
keyframe_points.add(frame_count)
- for i, frame in enumerate(record_list):
- keyframe_points[i].co = (
- t[i],
- all_trans_new[i][axis_i]
- )
+ for i in range(frame_count):
+ kp = keyframe_points[i]
+ kp.co = (t[i], all_trans_new[i][axis_i])
+ set_kp_linear_vector(kp)
+
+ curve.update()
def stop_record(ctx):
@@ -588,17 +645,21 @@
if source_obj is None or source_obj != obj:
continue
+ # ensure quaternion mode before baking
+ for pb in obj.pose.bones:
+ pb.rotation_mode = 'QUATERNION'
+
is_direct = obj.rebocap_drive_type == 'DIRECT'
is_valid = False
if is_direct:
for pose_bone in obj.pose.bones:
- if is_direct:
- idx = get_pose_idx(pose_bone)
- if idx != -1:
- is_valid = True
- break
+ idx = get_pose_idx(pose_bone)
+ if idx != -1:
+ is_valid = True
+ break
if not is_valid:
continue
+
obj.animation_data_create()
obj.animation_data.action = action
dt = 1 / ctx.scene.render.fps
@@ -608,7 +669,8 @@
global obj_info_helper
obj_info = obj_info_helper.get_or_insert_obj(obj.name)
- # below is direct
+
+ # below is direct bake
for pose_bone in obj.pose.bones:
bone = obj.data.bones.get(pose_bone.name)
idx = get_pose_idx(pose_bone)
@@ -617,26 +679,38 @@
bone_info = obj_info.get_or_insert_bone(bone.name)
rest_matrix_to_world, rest_matrix_from_world = bone_info.get_rest_init()
+
rotation_quaternion = []
t = []
- for frame in record_list:
+
+ prev_q = None
+ for i, frame in enumerate(record_list):
t.append((frame[3] - begin_t) / dt + 1)
pose = frame[1][idx]
new_pose = rest_matrix_from_world @ Quaternion([pose[3], pose[0], pose[1], pose[2]]) @ rest_matrix_to_world
+ new_pose = quat_make_continuous(prev_q, new_pose)
rotation_quaternion.append(new_pose)
+ prev_q = new_pose
+
data_path = 'pose.bones["%s"].rotation_quaternion' % pose_bone.name
for axis_i in range(4):
- curve = action.fcurves.new(data_path=data_path, index=axis_i)
+ curve = ensure_fcurve(action, obj, data_path, axis_i)
keyframe_points = curve.keyframe_points
frame_count = len(rotation_quaternion)
keyframe_points.add(frame_count)
- for i, rotation_quaternion_item in enumerate(rotation_quaternion):
- keyframe_points[i].co = (
- t[i],
- rotation_quaternion_item[axis_i]
- )
+ for i, q in enumerate(rotation_quaternion):
+ kp = keyframe_points[i]
+ kp.co = (t[i], q[axis_i])
+ kp.interpolation = 'LINEAR'
+ kp.handle_left_type = 'VECTOR'
+ kp.handle_right_type = 'VECTOR'
+ curve.update()
+
+ # KEY ROOT LOCATION ON THE ACTUAL ROOT BONE (matches streaming behavior)
if idx == 0 and obj_info.root_bone is not None:
- data_path = 'pose.bones["%s"].location' % pose_bone.name
+ root_pb = obj_info.root_bone
+ data_path = f'pose.bones["{root_pb.name}"].location'
+
all_trans_new = []
for i, frame in enumerate(record_list):
trans = frame[0]
@@ -648,53 +722,68 @@
all_trans_new.append(new_local_matrix.translation)
for axis_i in range(3):
- curve = action.fcurves.new(data_path=data_path, index=axis_i)
+ curve = ensure_fcurve(action, obj, data_path, axis_i)
keyframe_points = curve.keyframe_points
frame_count = len(record_list)
keyframe_points.add(frame_count)
for i, frame in enumerate(record_list):
- keyframe_points[i].co = (
- t[i],
- all_trans_new[i][axis_i]
- )
- for cu in action.fcurves:
- for bez in cu.keyframe_points:
- bez.interpolation = 'LINEAR'
+ kp = keyframe_points[i]
+ kp.co = (t[i], all_trans_new[i][axis_i])
+ kp.interpolation = 'LINEAR'
+ kp.handle_left_type = 'VECTOR'
+ kp.handle_right_type = 'VECTOR'