1
0
forked from Rowland/EG
EG/core/vr_actions.py
2025-09-15 16:41:35 +08:00

583 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)对于vector2float对于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动作管理器已清理")