Bootstrap

Blender使用脚本控制骨骼旋转2.0

下位机读取传感器四元数(四元数是一种用于表示三维空间中旋转的方法,它由一个实部和三个虚部组成),通过串口读取数据并更新 Blender 中的骨骼(Armature)的旋转。代码结合了 Blender 的 Python API 和串口通信,主要用于将外部设备(如传感器或运动捕捉系统)的数据映射到 Blender 中的骨骼上。

一.功能概述


下位机读取传感器四元数(四元数是一种用于表示三维空间中旋转的方法,它由一个实部和三个虚部组成),通过串口读取数据并更新 Blender 中的骨骼(Armature)的旋转。这段代码结合了 Blender 的 Python API 和串口通信,主要用于将外部设备(如传感器或运动捕捉系统)的数据映射到 Blender 中的骨骼上。跟上一篇帖子不一样的是这一篇处理了10个关节的数据,上一篇只处理了2个。本篇将代码集成在一个python file中,代码解释会比上一篇更加详细。下位机代码在上上篇帖子。

上一篇帖子里面有关于使用四元数控制骨骼旋转的详细解释,在这里就不赘述了,导航:Blender使用脚本控制骨骼旋转1.0

二.代码解析部分

1.导入必要的库

这些库包括Blender的API、时间处理、系统操作、串口通信和科学计算等。

import bpy
import time
import sys
import serial
import glob
import numpy as np
  • bpy: Blender的Python API,用于操控Blender环境。
  • time: 时间相关的操作。
  • sys: 系统级操作,如修改路径。
  • serial: 用于串口通信。
  • glob: 文件路径操作,但在本代码中未实际使用。
  • numpy: 科学计算库,主要用于四元数运算。

2.串口配置

设置串口端口和波特率,并尝试打开串口。

FPS = 60
# 串口配置
port = 'COM10'  # 指定串口号,根据实际情况修改
baudrate = 57600  # 波特率,根据实际情况修改

# 创建串口对象
ser = serial.Serial(port, baudrate, timeout=1)
if ser.isOpen():
    print(f"串口 {port} 已打开,波特率 {baudrate}")
else:
    print(f"无法打开串口 {port}")
  • 设置帧率FPS为60。
  • 配置串口端口port和波特率baudrate,并尝试打开串口。如果成功打开,打印成功信息,否则打印失败信息。

3.获取Blender场景和骨骼对象与骨骼的具体部分

获取当前Blender场景和名为“骨架”的骨骼对象。

# # Get the whole bge scene
# scene = bge.logic.getCurrentScene()
# # Helper vars for convenience
# source = scene.objects
# # Get the whole Armature
# main_arm = source.get('Armature')
# ob = bge.logic.getCurrentController().owner
# 获取当前场景
scene = bpy.context.scene
# 获取 'Armature' 骨骼对象
armature = bpy.data.objects["骨架"]
# 获取当前对象(这里假设是骨骼对象的所有者,可以根据实际情况修改)
ob = bpy.context.active_object

# get the bones we need
bone_upper_arm_R = armature.pose.bones.get("armUp.R")
bone_lower_arm_R = armature.pose.bones.get("armDown.R")
bone_upper_arm_L = armature.pose.bones.get("armUp.L")
bone_lower_arm_L = armature.pose.bones.get("armDown.L")
bone_trunk = armature.pose.bones.get("trunk.001")
bone_head = armature.pose.bones.get("head.001")
bone_upper_leg_R = armature.pose.bones.get("legUp.R.001")
bone_lower_leg_R = armature.pose.bones.get("legDown.R.001")
bone_upper_leg_L = armature.pose.bones.get("legUp.L.001")
bone_lower_leg_L = armature.pose.bones.get("legDown.L.001")
  • 获取当前Blender场景。
  • 获取名为骨架的骨骼对象。
  • 获取当前活动对象。

4.定义四元数运算函数

四元数乘法函数,用于旋转计算。

def multiplyQuaternion(q1, q0):
    w0, x0, y0, z0 = q0
    w1, x1, y1, z1 = q1
    return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
                     x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
                     -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
                     x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)

5.设置骨骼旋转的函数

根据四元数设置骨骼的旋转。

def setBoneRotation(bone, rotation):
    w, x, y, z = rotation
    bone.rotation_quaternion[0] = w
    bone.rotation_quaternion[1] = x
    bone.rotation_quaternion[2] = y
    bone.rotation_quaternion[3] = z

关于四元数控制骨骼旋转看上一篇就行,这里不再赘述。导航一下:Blender使用脚本控制骨骼旋转1.0

6.更新角度的函数(!!重点!!)

根据从串口接收到的数据更新骨骼的旋转。该函数 updateAngles 是整个代码的核心之一。它的作用是将从串口接收到的角度数据应用到Blender中的骨骼Armature上。具体来说,它首先将输入的角度数据转换为适当的格式,然后计算各个骨骼部分的相对旋转,并最终将这些旋转应用到相应的骨骼上。

①将角度数据转换位NumPy数组:
  • 以便进行后续的四元数运算。
lowerarmR_out = np.array([angles[0][0], angles[0][1], angles[0][2], angles[0][3]])
... ...
②计算各个骨骼部分的相对旋转:
  • trunk_invtrunk_out 的共轭四元数,用于计算 head_rel
  • head_rel 是头部相对于躯干的旋转。
  • upperarmR_inv 是右上臂的共轭四元数,用于计算 lowerarmR_rel
  • 依此类推,计算其他骨骼部分的相对旋转。
trunk_inv = trunk_out * np.array([1, -1, -1, -1])
head_rel = multiplyQuaternion(trunk_inv, head_out)
upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
... ...
③应用旋转到骨骼:
  • 使用 setBoneRotation 函数,将计算好的四元数旋转应用到各个骨骼部位。
setBoneRotation(bone_trunk, trunk_out)         
setBoneRotation(bone_upper_arm_R, upperarmR_out)
setBoneRotation(bone_lower_arm_R, lowerarmR_rel)
... ...

总之,updateAngles 函数从串口接收到的角度数据中提取并转换为NumPy数组。然后,它计算各个骨骼部分的相对旋转,并将这些旋转应用到Blender中的相应骨骼上。通过这种方式,可以将外部设备的运动数据实时映射到Blender中的骨骼动画上,实现动态动画效果。

7.重置骨骼的函数

  • 将所有骨骼的旋转重置为默认状态。
def resetBone():
    setBoneRotation(bone_upper_arm_R, [1, 0, 0, 0])
    # 重置其他骨骼的旋转在这里插入代码片

8.定义Blender操作符类 —— Modal Timer Operator 类(!!重点!!)

处理定时器事件,每次定时器触发时从串口读取数据并更新骨骼。

①字段解释
class ModalTimerOperator(bpy.types.Operator):
    # we need these two fields for Blender
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"
    
    _timer = None
  • bl_idname:这是 Blender 中操作符的唯一标识符,通常以 wm. 开头。
  • bl_label:这是操作符的名称,会在 Blender 的用户界面中显示。
  • _timer:这是一个私有字段,用于存储定时器对象。
②modal方法
1)事件处理
    def modal(self, context, event):
        if event.type == "ESC":
            print("BlenderTimer received ESC.")
            return self.cancel(context)
    
        if event.type == "TIMER":
  • modal函数在 Blender 中通过定时器事件和键盘事件被反复调用。
  • event.type == "ESC":如果检测到按下 ESC 键,调用 self.cancel(context) 方法取消操作。
  • event.type == "TIMER":定时器事件,用于定期从串口读取数据并更新角度。
2)读取串口数据并验证数据有效性
            try:
                # Read serial data
                s = ser.readline()[:-3].decode('UTF-8')  # delete ";\r\n"
            except Exception as e:
                print(f"Error reading serial data: {e}")
                return {"PASS_THROUGH"}
            if not s:
                print("Invalid joint data")
                return {"PASS_THROUGH"}
  • 尝试从串口读取一行数据,并去掉末尾的;\r\n 字符。
  • 如果读取失败,打印错误信息并返回 "PASS_THROUGH",继续处理其他事件。
  • 如果读取的数据为空,打印错误信息并返回"PASS_THROUGH"
3)处理关节角度数据
            # Process joint angles
            angles = [x.split(',') for x in s.split(';')]
  • 将串口读取的数据按分割成多个关节数据,然后再按分割成单个角度值。
4)转换数据类型
            try:
                for i in range(len(angles)):
                    angles[i] = [float(x) for x in angles[i]]
            except ValueError as ve:
                print(f"Error converting string to float: {ve}")
                return {"PASS_THROUGH"}
  • 尝试将每个角度值从字符串转换为浮点数。
  • 如果转换失败,打印错误信息并返回 "PASS_THROUGH"
5)调用更新函数
            if len(angles) == 10:
                updateAngles(angles)

            #如果刷新率太低,取消这一行的注释以强制Blender渲染视窗
            # bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
    
  • 检查解析出的角度数据是否包含 10 个关节的数据。
  • 如果数据完整,调用 updateAngles(angles) 函数更新骨骼角度。

总之,modal函数通过定时器事件定期从串口读取关节角度数据,并将这些数据应用到 Blender 模型的骨骼上。该过程包括读取和验证数据、解析和转换数据类型、以及调用更新函数。这种方法使得从外部设备(如传感器)获取的实时运动数据能够动态映射到 Blender 中的 3D 模型,实现动画效果。

③execute方法

该方法在操作符启动时调用,初始化定时器并将操作符添加到 Blender 的事件处理器中。

    def execute(self, context):
        # update rate is 0.01 second
        self._timer = context.window_manager.event_timer_add(1./FPS, window=context.window)
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}
  • context.window_manager.event_timer_add(1./FPS, window=context.window):添加定时器,更新速率为每秒 FPS 次。
  • context.window_manager.modal_handler_add(self):将操作符添加到 Blender 的事件处理器中。
  • 返回 "RUNNING_MODAL" 表示操作符正在运行。
④cancel方法

该方法在操作符取消时调用,关闭串口,重置关节位置,并移除定时器。

    def cancel(self, context):
        ser.close()
        
        # reset joint position
        resetBone()
        context.window_manager.event_timer_remove(self._timer)
        print("BlenderTimer Stopped.")
        return {"CANCELLED"}
  • ser.close():关闭串口。
  • resetBone():重置关节位置。
  • context.window_manager.event_timer_remove(self._timer):移除定时器。
  • 打印消息并返回 "CANCELLED" 表示操作符已取消。

总之,ModalTimerOperator 类通过定时器事件定期从串口读取关节角度数据,并将这些数据应用到 Blender 模型的骨骼上。该类的实现包括事件处理、定时器管理、数据读取和处理,以及操作符的启动和取消操作。这种方法使得从外部设备(如传感器)获取的实时运动数据能够动态映射到 Blender 中的 3D 模型,实现动画效果。

9.主程序

注册并启动操作符,处理串口数据,捕获异常以进行清理工作。

if __name__ == "__main__":
    try:
        print("Starting services.")
        bpy.utils.register_class(ModalTimerOperator)
        
        # start Blender timer
        bpy.ops.wm.modal_timer_operator()
        
        print("All started.")
    except KeyboardInterrupt:
        resetBone()
        ser.close()
        print("Received KeyboardInterrupt, stopped.")
  • 注册并启动 ModalTimerOperator 操作符。
  • 在脚本运行时,启动Blender的定时器操作符并处理串口数据。
  • 捕获 KeyboardInterrupt 异常,停止骨骼更新并关闭串口通信。

三.完整代码部分

import bpy
import time

import sys
#sys.path.append("/usr/lib/python3/dist-packages")
import serial
import glob
import numpy as np


FPS = 60

# 串口配置
port = 'COM10'  # 指定串口号,根据实际情况修改
baudrate = 57600  # 波特率,根据实际情况修改

# 创建串口对象
ser = serial.Serial(port, baudrate, timeout=1)
if ser.isOpen():
    print(f"串口 {port} 已打开,波特率 {baudrate}")
else:
    print(f"无法打开串口 {port}")

#Connect the suit first and after a ~second launch the script


# # Get the whole bge scene
# scene = bge.logic.getCurrentScene()
# # Helper vars for convenience
# source = scene.objects
# # Get the whole Armature
# main_arm = source.get('Armature')
# ob = bge.logic.getCurrentController().owner
# 获取当前场景
scene = bpy.context.scene
# 获取 'Armature' 骨骼对象
armature = bpy.data.objects["骨架"]
# 获取当前对象(这里假设是骨骼对象的所有者,可以根据实际情况修改)
ob = bpy.context.active_object

# get the bones we need
bone_upper_arm_R = armature.pose.bones.get("armUp.R")
bone_lower_arm_R = armature.pose.bones.get("armDown.R")
bone_upper_arm_L = armature.pose.bones.get("armUp.L")
bone_lower_arm_L = armature.pose.bones.get("armDown.L")
bone_trunk = armature.pose.bones.get("trunk.001")
bone_head = armature.pose.bones.get("head.001")
bone_upper_leg_R = armature.pose.bones.get("legUp.R.001")
bone_lower_leg_R = armature.pose.bones.get("legDown.R.001")
bone_upper_leg_L = armature.pose.bones.get("legUp.L.001")
bone_lower_leg_L = armature.pose.bones.get("legDown.L.001")

def multiplyQuaternion(q1, q0):
    w0, x0, y0, z0 = q0
    w1, x1, y1, z1 = q1
    return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
                     x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
                     -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
                     x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)

def setBoneRotation(bone, rotation):
    w, x, y, z = rotation
    bone.rotation_quaternion[0] = w
    bone.rotation_quaternion[1] = x
    bone.rotation_quaternion[2] = y
    bone.rotation_quaternion[3] = z
    
def updateAngles(angles):
    lowerarmR_out = np.array([angles[0][0],angles[0][1],angles[0][2],angles[0][3]])
    upperarmR_out = np.array([angles[1][0],angles[1][1],angles[1][2],angles[1][3]])
    lowerarmL_out = np.array([angles[2][0],angles[2][1],angles[2][2],angles[2][3]])
    upperarmL_out = np.array([angles[3][0],angles[3][1],angles[3][2],angles[3][3]])
    trunk_out = np.array((angles[4][0],angles[4][1],angles[4][2],angles[4][3]))
    upperLegR_out = np.array((angles[5][0],angles[5][1],angles[5][2],angles[5][3]))
    lowerLegR_out = np.array((angles[6][0],angles[6][1],angles[6][2],angles[6][3]))
    upperLegL_out = np.array((angles[7][0],angles[7][1],angles[7][2],angles[7][3]))
    lowerLegL_out = np.array((angles[8][0],angles[8][1],angles[8][2],angles[8][3]))
    head_out = np.array((angles[9][0],angles[9][1],angles[9][2],angles[9][3]))

#    upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
#    lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
    trunk_inv = trunk_out * np.array([1, -1, -1, -1])
    head_rel = multiplyQuaternion(trunk_inv, head_out)
#    upperarmR_rel = multiplyQuaternion(trunk_inv, upperarmR_out)
    upperarmR_inv = upperarmR_out * np.array([1, -1, -1, -1])
    lowerarmR_rel = multiplyQuaternion(upperarmR_inv, lowerarmR_out)
    upperarmL_inv = upperarmL_out * np.array([1, -1, -1, -1])
    lowerarmL_rel = multiplyQuaternion(upperarmL_inv, lowerarmL_out)
    upperLegR_inv = upperLegR_out * np.array([1, -1, -1, -1])
    lowerLegR_rel = multiplyQuaternion(upperLegR_inv, lowerLegR_out)
    upperLegL_inv = upperLegL_out * np.array([1, -1, -1, -1])
    lowerLegL_rel = multiplyQuaternion(upperLegL_inv, lowerLegL_out)
    
    setBoneRotation(bone_trunk, trunk_out)         
    setBoneRotation(bone_upper_arm_R, upperarmR_out)
    setBoneRotation(bone_lower_arm_R, lowerarmR_rel)
    setBoneRotation(bone_upper_arm_L, upperarmL_out)
    setBoneRotation(bone_lower_arm_L, lowerarmL_rel)
    setBoneRotation(bone_head, head_rel)
    setBoneRotation(bone_upper_leg_R, upperLegR_out)
    setBoneRotation(bone_lower_leg_R, lowerLegR_rel)
    setBoneRotation(bone_upper_leg_L, upperLegL_out)
    setBoneRotation(bone_lower_leg_L, lowerLegL_rel)

    
    
def resetBone():
    setBoneRotation(bone_upper_arm_R, [1, 0, 0, 0])
    setBoneRotation(bone_lower_arm_R, [1, 0, 0, 0])
    setBoneRotation(bone_upper_arm_L, [1, 0, 0, 0])
    setBoneRotation(bone_lower_arm_L, [1, 0, 0, 0])
    setBoneRotation(bone_trunk, [1, 0, 0, 0])
    setBoneRotation(bone_head, [1, 0, 0, 0])
    setBoneRotation(bone_upper_leg_R, [1, 0, 0, 0])
    setBoneRotation(bone_lower_leg_R, [1, 0, 0, 0])
    setBoneRotation(bone_upper_leg_L, [1, 0, 0, 0])
    setBoneRotation(bone_lower_leg_L, [1, 0, 0, 0])

class ModalTimerOperator(bpy.types.Operator):
    # we need these two fields for Blender
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"
    
    _timer = None
    
    def modal(self, context, event):
        if event.type == "ESC":
            print("BlenderTimer received ESC.")
            return self.cancel(context)
    
        if event.type == "TIMER":
            try:
                # Read serial data
                s = ser.readline()[:-3].decode('UTF-8')  # delete ";\r\n"
            except Exception as e:
                print(f"Error reading serial data: {e}")
                return {"PASS_THROUGH"}
            if not s:
                print("Invalid joint data")
                return {"PASS_THROUGH"}
            # Process joint angles
            angles = [x.split(',') for x in s.split(';')]

            try:
                for i in range(len(angles)):
                    angles[i] = [float(x) for x in angles[i]]
            except ValueError as ve:
                print(f"Error converting string to float: {ve}")
                return {"PASS_THROUGH"}

            if len(angles) == 10:
                updateAngles(angles)

            # if refresh rate is too low, uncomment this line to force Blender to render viewport
            # bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
    
        return {"PASS_THROUGH"}

    def execute(self, context):
        # update rate is 0.01 second
        self._timer = context.window_manager.event_timer_add(1./FPS, window=context.window)
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}
        
    def cancel(self, context):
        ser.close()
        
        # reset joint position
        resetBone()
        context.window_manager.event_timer_remove(self._timer)
        print("BlenderTimer Stopped.")
        return {"CANCELLED"}


if __name__ == "__main__":
    try:
        print("Starting services.")
        bpy.utils.register_class(ModalTimerOperator)
        
        # start Blender timer
        bpy.ops.wm.modal_timer_operator()
        
        print("All started.")
    except KeyboardInterrupt:
        resetBone()
        ser.close()
        print("Received KeyboardInterrupt, stopped.")

​代码参考这位大佬:【STM32 MPU6050 动作捕捉完结】
所有代码都在大佬的GIT: GIT

Blender通信脚本可参考:
【笔记 #12: MPU6050 三轴陀螺仪实现手臂关节动作捕捉 + Blender 实时串流同步】
MotioSuit:github

侵删

;