Arduino 2 Blender 串口通讯实现动作捕捉
整个架构是:
Arduino 读取 4 个电位器的模拟电压值,通过 ADC 采集并转换为浮点数据,通过串口发送给上位机(电脑)
上位机上运行 Blender 中的 Python 代码,读取串口数据并转换,将数据转送给全局的关节数据字典,同时另一个线程中读取字典的值并将关节角度绑定到骨架上
Arduino 程序
/** Blender_Serial_Comm_Arduino_Code.ino
*
* Demonstrates reading four(4) potentiometer sensor readings from a robotics arm
* and sending the data to Blender running Python via the Serial port.
*/
/* 取消下面这样的注释来输出 DEBUG 信息 | uncomment the line below to view floating point data via Serial monitor. */
//#define DEBUG
void setup() {
/* 初始化串口通讯 | setup the serial port */
Serial.begin(115200);
}
void loop() {
float data[4];
/* 读取四个电位器的值 | read and process the potentiometer data */
data[0] = analogRead(A0) - 100;
data[1] = analogRead(A1) - 500;
data[2] = analogRead(A2) - 540;
data[3] = analogRead(A3) - 870;
data[0] *= 0.0013;
data[1] *= 0.0013;
data[2] *= 0.0013;
data[3] *= 0.0013;
#ifndef DEBUG
for (int i=0; i<4; i+=1) {
/* 将 32 位的 float 型转换为 4 个字节并从串口输出
| convert the 32-bit floating point into 4 8-bit uint8_t buffer array */
uint8_t *buffer;
buffer = (uint8_t *)&data[i];
Serial.write(buffer[0]);
Serial.write(buffer[1]);
Serial.write(buffer[2]);
Serial.write(buffer[3]);
}
#else
Serial.print(data[0]); Serial.print("\t");
Serial.print(data[1]); Serial.print("\t");
Serial.print(data[2]); Serial.print("\t");
Serial.print(data[3]); Serial.print("\t");
#endif
/* 输出一个换行代表消息结束 | this is the end-of-message flag */
Serial.print("\n\n");
delay(20);
}
Python 这边需要安装 pyserial 这个第三方库,而且需要安装到 Blender 自带的 py 环境中
在 Blender 中的 Python 是独立于系统的单独环境,所以需要在这个环境下安装第三方的库。方法如下:
在 Blender 的 Python Terminal 中输入
import sys
sys.exec_prefix
即可得到 Blender 的 Python 的文件位置
打开 Powershell, cd 到那个文件路径下的 bin 文件夹中 (python 的版本是 3+,那个 2.82 是 Blender 版本)
cd D:\Documents\Blender\2.82\python\bin
该文件路径下有一个 python.exe,这个就是 Blender 所使用的 Python 文件
运行 pip
.\python.exe -m pip install pyserial
现在即可在 Blender 中调用这个第三方 serial 库
Blender Python 程序
import math
import struct
from threading import Thread
import bpy
import serial
# 定义全局变量 | define global variables
stop_flag = 0
position_data = []
# 从 Blender 获取场景物体 | get the blender scene objects
# 先获取骨架
armature = bpy.data.objects["Armature"]
bone0 = armature.pose.bones.get("Bone")
bone0.rotation_mode = AXIS_ANGLE
bone1 = armature.pose.bones.get("Bone.001")
bone1.rotation_mode = AXIS_ANGLE
# 这里改变一下骨头的旋转轴,设置成围绕 y 轴旋转
bone2 = armature.pose.bones.get("Bone.002")
bone2.rotation_mode = AXIS_ANGLE
bone2.rotation_axis_angle[1:] = [0, 1, 0]
bone3 = armature.pose.bones.get("Bone.003")
bone3.rotation_mode = AXIS_ANGLE
# 定义一个定时器来负责场景的刷新
class ModalTimerOperator(bpy.types.Operator):
# 这两个名字是必须的,是 Blender 的标识符
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
def __init__(self):
pass
def modal(self, context, event):
global stop_flag
# 侦听 Blender 窗口的活动,如果按下了 ESC 键就停止所有线程
if event.type == ESC:
stop_flag = 1
# bpy.utils.unregister_class(ModalTimerOperator)
return self.cancel(context)
# 如果是定时器更新,说明我们需要刷新页面了
if event.type == TIMER:
# 确保接收到串口的数据后,更新四个骨骼的旋转
if len(position_data) >= 4:
bone0.rotation_axis_angle[0] = (position_data[0] * math.pi)
bone1.rotation_axis_angle[0] = (position_data[1] * math.pi)
bone2.rotation_axis_angle[0] = (position_data[2] * math.pi)
bone3.rotation_axis_angle[0] = -(position_data[3] * math.pi)
# 返回 "PASS_THROUGH",这样可以让其他 Handler 依旧接收到所有事件
return {PASS_THROUGH}
def execute(self, context):
# 启动计时器,间隔为 0.1 秒
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
context.window_manager.modal_handler_add(self)
return {RUNNING_MODAL}
def cancel(self, context):
# 停止计时器
context.window_manager.event_timer_remove(self._timer)
return {CANCELLED}
# 定义一个从串口读取数据的函数
def readData(ser):
c = b""
buffer = b""
while True:
c = ser.read(1)
if c == b"\n":
# detect for end-of-message flag
if ser.read(1) == b"\n":
break
buffer += c
return buffer
# 这个函数负责管理串口的连接和通讯
def serialCommHandler():
global position_data
# 开启串口 | open serial port
ser = serial.Serial(port="COM3", baudrate=115200)
while not stop_flag:
try:
# 读取串口数据并按照四个 float 的格式解码
position_data = struct.unpack("<ffff", readData(ser))
except Exception as e:
print(e)
print("Exit condition detected.")
ser.close()
if __name__ == "__main__":
bpy.utils.register_class(ModalTimerOperator)
# 串口通讯在子线程中运行
t = Thread(target=serialCommHandler, args=())
t.start()
# 启动 Blender 的 timer | run blender loop
bpy.ops.wm.modal_timer_operator()
另外还有一段单独的 Python 程序用来调试
""" read_serial.py
"""
import struct
import serial
ser = serial.Serial(port="COM3", baudrate=115200)
print(ser.name)
def readData():
c = b""
buffer = b""
while True:
c = ser.read(1)
if c == b"\n":
if ser.read(1) == b"\n":
break
buffer += c
return buffer
while True:
data = struct.unpack("<ffff", readData())
print(data)