forked from Rowland/EG
583 lines
20 KiB
Python
583 lines
20 KiB
Python
"""
|
||
VR动作系统模块
|
||
|
||
基于OpenVR Action系统,提供高级的输入处理和动作映射:
|
||
- VR动作清单管理
|
||
- 按钮和轴输入处理
|
||
- 触觉反馈
|
||
- 动作集管理
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
from direct.showbase.DirectObject import DirectObject
|
||
|
||
try:
|
||
import openvr
|
||
OPENVR_AVAILABLE = True
|
||
except ImportError:
|
||
OPENVR_AVAILABLE = False
|
||
|
||
|
||
class VRActionManager(DirectObject):
|
||
"""VR动作管理器 - 处理OpenVR动作系统"""
|
||
|
||
def __init__(self, vr_manager):
|
||
"""初始化VR动作管理器
|
||
|
||
Args:
|
||
vr_manager: VR管理器实例
|
||
"""
|
||
super().__init__()
|
||
|
||
self.vr_manager = vr_manager
|
||
self.vr_input = None
|
||
self.action_set_handles = []
|
||
self.action_handles = {}
|
||
|
||
# 预定义的标准动作
|
||
self.standard_actions = {
|
||
# 姿态动作
|
||
'pose': '/actions/default/in/Pose',
|
||
|
||
# 按钮动作
|
||
'trigger': '/actions/default/in/Trigger',
|
||
'grip': '/actions/default/in/Grip',
|
||
'menu': '/actions/default/in/Menu',
|
||
'system': '/actions/default/in/System',
|
||
'trackpad_click': '/actions/default/in/TrackpadClick',
|
||
'trackpad_touch': '/actions/default/in/TrackpadTouch',
|
||
'a_button': '/actions/default/in/AButton',
|
||
'b_button': '/actions/default/in/BButton',
|
||
|
||
# 轴动作
|
||
'trackpad': '/actions/default/in/Trackpad',
|
||
'joystick': '/actions/default/in/Joystick',
|
||
'squeeze': '/actions/default/in/Squeeze',
|
||
|
||
# 震动输出
|
||
'haptic': '/actions/default/out/Haptic'
|
||
}
|
||
|
||
# 动作集
|
||
self.default_action_set = '/actions/default'
|
||
|
||
print("✓ VR动作管理器初始化完成")
|
||
|
||
def initialize(self):
|
||
"""初始化VR动作系统"""
|
||
if not OPENVR_AVAILABLE or not self.vr_manager.vr_system:
|
||
print("⚠️ VR系统不可用,无法初始化动作系统")
|
||
return False
|
||
|
||
try:
|
||
print("🎮 正在初始化VR动作系统...")
|
||
|
||
# 获取VR输入接口
|
||
self.vr_input = openvr.VRInput()
|
||
if not self.vr_input:
|
||
print("❌ 无法获取VR输入接口")
|
||
return False
|
||
|
||
# 创建动作清单文件
|
||
manifest_path = self._create_action_manifest()
|
||
if not manifest_path:
|
||
print("❌ 无法创建动作清单")
|
||
return False
|
||
|
||
# 加载动作清单
|
||
error = self.vr_input.setActionManifestPath(manifest_path)
|
||
if error != openvr.VRInputError_None:
|
||
print(f"❌ 加载动作清单失败: {error}")
|
||
return False
|
||
|
||
# 获取动作句柄
|
||
self._load_action_handles()
|
||
|
||
# 创建动作集
|
||
self._setup_action_sets()
|
||
|
||
print("✅ VR动作系统初始化成功")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ VR动作系统初始化失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def _create_action_manifest(self):
|
||
"""创建VR动作清单文件"""
|
||
try:
|
||
# 动作清单配置
|
||
manifest_data = {
|
||
"actions": [
|
||
# 姿态动作
|
||
{
|
||
"name": "/actions/default/in/Pose",
|
||
"type": "pose"
|
||
},
|
||
|
||
# 数字动作(按钮)
|
||
{
|
||
"name": "/actions/default/in/Trigger",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/Grip",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/Menu",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/System",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/TrackpadClick",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/TrackpadTouch",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/AButton",
|
||
"type": "boolean"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/BButton",
|
||
"type": "boolean"
|
||
},
|
||
|
||
# 模拟动作(轴)
|
||
{
|
||
"name": "/actions/default/in/Trackpad",
|
||
"type": "vector2"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/Joystick",
|
||
"type": "vector2"
|
||
},
|
||
{
|
||
"name": "/actions/default/in/Squeeze",
|
||
"type": "vector1"
|
||
},
|
||
|
||
# 震动输出
|
||
{
|
||
"name": "/actions/default/out/Haptic",
|
||
"type": "vibration"
|
||
}
|
||
],
|
||
|
||
"action_sets": [
|
||
{
|
||
"name": "/actions/default",
|
||
"usage": "single"
|
||
}
|
||
],
|
||
|
||
"default_bindings": [
|
||
{
|
||
"controller_type": "vive_controller",
|
||
"binding_url": "bindings_vive.json"
|
||
},
|
||
{
|
||
"controller_type": "oculus_touch",
|
||
"binding_url": "bindings_oculus.json"
|
||
},
|
||
{
|
||
"controller_type": "knuckles",
|
||
"binding_url": "bindings_index.json"
|
||
}
|
||
],
|
||
|
||
"localization": [
|
||
{
|
||
"language_tag": "zh_CN",
|
||
"/actions/default/in/Trigger": "扳机",
|
||
"/actions/default/in/Grip": "握把",
|
||
"/actions/default/in/Menu": "菜单",
|
||
"/actions/default/in/System": "系统",
|
||
"/actions/default/in/TrackpadClick": "触摸板点击",
|
||
"/actions/default/in/TrackpadTouch": "触摸板触摸",
|
||
"/actions/default/in/Pose": "手部姿态",
|
||
"/actions/default/out/Haptic": "震动反馈"
|
||
}
|
||
]
|
||
}
|
||
|
||
# 保存到临时目录
|
||
manifest_dir = Path.cwd() / "vr_actions"
|
||
manifest_dir.mkdir(exist_ok=True)
|
||
|
||
manifest_path = manifest_dir / "actions.json"
|
||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||
json.dump(manifest_data, f, indent=2, ensure_ascii=False)
|
||
|
||
# 创建基本的绑定文件
|
||
self._create_default_bindings(manifest_dir)
|
||
|
||
print(f"✓ 动作清单已创建: {manifest_path}")
|
||
return str(manifest_path)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建动作清单失败: {e}")
|
||
return None
|
||
|
||
def _create_default_bindings(self, manifest_dir):
|
||
"""创建默认的控制器绑定文件"""
|
||
# Vive控制器绑定
|
||
vive_bindings = {
|
||
"controller_type": "vive_controller",
|
||
"description": "Vive控制器绑定",
|
||
"name": "EG VR Editor - Vive",
|
||
"bindings": {
|
||
"/actions/default": {
|
||
"sources": [
|
||
{
|
||
"inputs": {
|
||
"click": {
|
||
"output": "/actions/default/in/Trigger"
|
||
}
|
||
},
|
||
"mode": "button",
|
||
"path": "/user/hand/left/input/trigger"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"click": {
|
||
"output": "/actions/default/in/Trigger"
|
||
}
|
||
},
|
||
"mode": "button",
|
||
"path": "/user/hand/right/input/trigger"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"click": {
|
||
"output": "/actions/default/in/Grip"
|
||
}
|
||
},
|
||
"mode": "button",
|
||
"path": "/user/hand/left/input/grip"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"click": {
|
||
"output": "/actions/default/in/Grip"
|
||
}
|
||
},
|
||
"mode": "button",
|
||
"path": "/user/hand/right/input/grip"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"click": {
|
||
"output": "/actions/default/in/Menu"
|
||
}
|
||
},
|
||
"mode": "button",
|
||
"path": "/user/hand/left/input/menu"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"position": {
|
||
"output": "/actions/default/in/Trackpad"
|
||
},
|
||
"click": {
|
||
"output": "/actions/default/in/TrackpadClick"
|
||
},
|
||
"touch": {
|
||
"output": "/actions/default/in/TrackpadTouch"
|
||
}
|
||
},
|
||
"mode": "trackpad",
|
||
"path": "/user/hand/left/input/trackpad"
|
||
},
|
||
{
|
||
"inputs": {
|
||
"position": {
|
||
"output": "/actions/default/in/Trackpad"
|
||
},
|
||
"click": {
|
||
"output": "/actions/default/in/TrackpadClick"
|
||
},
|
||
"touch": {
|
||
"output": "/actions/default/in/TrackpadTouch"
|
||
}
|
||
},
|
||
"mode": "trackpad",
|
||
"path": "/user/hand/right/input/trackpad"
|
||
}
|
||
],
|
||
"poses": [
|
||
{
|
||
"output": "/actions/default/in/Pose",
|
||
"path": "/user/hand/left/pose/raw"
|
||
},
|
||
{
|
||
"output": "/actions/default/in/Pose",
|
||
"path": "/user/hand/right/pose/raw"
|
||
}
|
||
],
|
||
"haptics": [
|
||
{
|
||
"output": "/actions/default/out/Haptic",
|
||
"path": "/user/hand/left/output/haptic"
|
||
},
|
||
{
|
||
"output": "/actions/default/out/Haptic",
|
||
"path": "/user/hand/right/output/haptic"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
|
||
bindings_path = manifest_dir / "bindings_vive.json"
|
||
with open(bindings_path, 'w', encoding='utf-8') as f:
|
||
json.dump(vive_bindings, f, indent=2)
|
||
|
||
print(f"✓ Vive控制器绑定已创建: {bindings_path}")
|
||
|
||
def _load_action_handles(self):
|
||
"""加载动作句柄"""
|
||
if not self.vr_input:
|
||
return
|
||
|
||
try:
|
||
for action_name, action_path in self.standard_actions.items():
|
||
handle = self.vr_input.getActionHandle(action_path)
|
||
self.action_handles[action_name] = handle
|
||
print(f"✓ 加载动作: {action_name} -> {handle}")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 加载动作句柄失败: {e}")
|
||
|
||
def _setup_action_sets(self):
|
||
"""设置动作集"""
|
||
if not self.vr_input:
|
||
return
|
||
|
||
try:
|
||
# 获取默认动作集句柄
|
||
action_set_handle = self.vr_input.getActionSetHandle(self.default_action_set)
|
||
self.action_set_handles = [action_set_handle]
|
||
|
||
print(f"✓ 动作集已设置: {self.default_action_set}")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 设置动作集失败: {e}")
|
||
|
||
def update_actions(self):
|
||
"""更新动作状态 - 每帧调用"""
|
||
if not self.vr_input or not self.action_set_handles:
|
||
return
|
||
|
||
try:
|
||
# 更新动作状态
|
||
action_sets = (openvr.VRActiveActionSet_t * len(self.action_set_handles))()
|
||
for i, action_set_handle in enumerate(self.action_set_handles):
|
||
action_sets[i].ulActionSet = action_set_handle
|
||
|
||
self.vr_input.updateActionState(action_sets)
|
||
|
||
except Exception as e:
|
||
# 限制错误输出频率
|
||
if not hasattr(self, '_last_action_error_frame'):
|
||
self._last_action_error_frame = 0
|
||
|
||
if hasattr(self.vr_manager, 'frame_count'):
|
||
if self.vr_manager.frame_count - self._last_action_error_frame > 300: # 每5秒输出一次
|
||
print(f"⚠️ 更新动作状态失败: {e}")
|
||
self._last_action_error_frame = self.vr_manager.frame_count
|
||
|
||
def is_digital_action_pressed(self, action_name, device_path=None):
|
||
"""检查数字动作是否被按下
|
||
|
||
Args:
|
||
action_name: 动作名称
|
||
device_path: 设备路径(可选)
|
||
|
||
Returns:
|
||
tuple: (是否按下, 设备路径)
|
||
"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return False, None
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
action_data = self.vr_input.getDigitalActionData(action_handle, device_handle)
|
||
|
||
if device_path and action_data.bActive:
|
||
origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin)
|
||
device_path = origin_info.devicePath
|
||
|
||
return action_data.bActive and action_data.bState, device_path
|
||
|
||
except Exception as e:
|
||
return False, None
|
||
|
||
def is_digital_action_just_pressed(self, action_name, device_path=None):
|
||
"""检查数字动作是否刚刚被按下(上升沿)"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return False, None
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
action_data = self.vr_input.getDigitalActionData(action_handle, device_handle)
|
||
|
||
if device_path and action_data.bActive:
|
||
origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin)
|
||
device_path = origin_info.devicePath
|
||
|
||
return action_data.bActive and action_data.bChanged and action_data.bState, device_path
|
||
|
||
except Exception as e:
|
||
return False, None
|
||
|
||
def is_digital_action_just_released(self, action_name, device_path=None):
|
||
"""检查数字动作是否刚刚被释放(下降沿)"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return False, None
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
action_data = self.vr_input.getDigitalActionData(action_handle, device_handle)
|
||
|
||
if device_path and action_data.bActive:
|
||
origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin)
|
||
device_path = origin_info.devicePath
|
||
|
||
return action_data.bActive and action_data.bChanged and not action_data.bState, device_path
|
||
|
||
except Exception as e:
|
||
return False, None
|
||
|
||
def get_analog_action_value(self, action_name, device_path=None):
|
||
"""获取模拟动作值
|
||
|
||
Args:
|
||
action_name: 动作名称
|
||
device_path: 设备路径(可选)
|
||
|
||
Returns:
|
||
tuple: (值, 设备路径) - 值为Vec2(x,y)对于vector2,float对于vector1
|
||
"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return None, None
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle)
|
||
|
||
if device_path and analog_data.bActive:
|
||
origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin)
|
||
device_path = origin_info.devicePath
|
||
|
||
if analog_data.bActive:
|
||
# 根据动作类型返回适当的值
|
||
from panda3d.core import Vec2
|
||
if action_name in ['trackpad', 'joystick']:
|
||
return Vec2(analog_data.x, analog_data.y), device_path
|
||
else:
|
||
return analog_data.x, device_path
|
||
|
||
return None, device_path
|
||
|
||
except Exception as e:
|
||
return None, None
|
||
|
||
def get_pose_action_data(self, action_name, device_path=None):
|
||
"""获取姿态动作数据"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return None
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
pose_data = self.vr_input.getPoseActionDataForNextFrame(
|
||
action_handle,
|
||
openvr.TrackingUniverseStanding,
|
||
device_handle
|
||
)
|
||
|
||
return pose_data
|
||
|
||
except Exception as e:
|
||
return None
|
||
|
||
def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None):
|
||
"""触发震动反馈
|
||
|
||
Args:
|
||
action_name: 震动动作名称
|
||
duration: 持续时间(秒)
|
||
frequency: 频率
|
||
amplitude: 振幅 (0.0-1.0)
|
||
device_path: 设备路径(可选)
|
||
"""
|
||
if not self.vr_input or action_name not in self.action_handles:
|
||
return False
|
||
|
||
try:
|
||
action_handle = self.action_handles[action_name]
|
||
device_handle = openvr.k_ulInvalidInputValueHandle
|
||
|
||
if device_path:
|
||
device_handle = self.vr_input.getInputSourceHandle(device_path)
|
||
|
||
# 触发震动
|
||
self.vr_input.triggerHapticVibrationAction(
|
||
action_handle,
|
||
0, # 开始时间
|
||
duration,
|
||
frequency,
|
||
amplitude,
|
||
device_handle
|
||
)
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 触发震动反馈失败: {e}")
|
||
return False
|
||
|
||
def cleanup(self):
|
||
"""清理资源"""
|
||
self.ignoreAll()
|
||
|
||
# 清理动作句柄
|
||
self.action_handles.clear()
|
||
self.action_set_handles.clear()
|
||
|
||
print("🧹 VR动作管理器已清理") |