添加手柄管理
This commit is contained in:
parent
6c9c4339f2
commit
ded28e0097
194
VR_GUIDE.md
194
VR_GUIDE.md
@ -1,194 +0,0 @@
|
||||
# VR模式使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目现已支持VR模式,通过SteamVR将Panda3D引擎的画面传输到VR头显设备。您可以在虚拟现实环境中体验和编辑3D场景。
|
||||
|
||||
## 系统要求
|
||||
|
||||
### 硬件要求
|
||||
- 支持SteamVR的VR头显设备(如HTC Vive、Oculus Rift、Valve Index等)
|
||||
- VR头显控制器(可选,用于交互)
|
||||
- 满足VR运行要求的计算机配置
|
||||
|
||||
### 软件要求
|
||||
- Windows 10/11 或 Linux(支持OpenVR)
|
||||
- SteamVR运行时
|
||||
- Python 3.7+
|
||||
- 项目依赖库(见安装步骤)
|
||||
|
||||
## 安装与设置
|
||||
|
||||
### 1. 安装VR依赖
|
||||
```bash
|
||||
# 安装OpenVR Python库
|
||||
pip install openvr==2.2.0
|
||||
|
||||
# 或者安装所有项目依赖
|
||||
pip install -r requirements/requirements.txt
|
||||
```
|
||||
|
||||
### 2. 安装SteamVR
|
||||
1. 安装Steam客户端
|
||||
2. 在Steam中搜索并安装"SteamVR"
|
||||
3. 连接您的VR头显设备
|
||||
4. 按照SteamVR设置向导完成房间设置
|
||||
|
||||
### 3. 验证VR设置
|
||||
运行VR测试脚本来验证安装:
|
||||
```bash
|
||||
python vr_test.py
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 启动VR模式
|
||||
|
||||
1. **启动SteamVR**
|
||||
- 在Steam中启动SteamVR
|
||||
- 确保VR头显已连接并处于就绪状态
|
||||
|
||||
2. **启动应用程序**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
3. **进入VR模式**
|
||||
- 在菜单栏中选择 `VR` → `进入VR模式`
|
||||
- 系统会自动检测VR设备并初始化VR渲染
|
||||
|
||||
4. **戴上VR头显**
|
||||
- 现在您可以在VR环境中查看和操作3D场景
|
||||
|
||||
### VR菜单选项
|
||||
|
||||
- **进入VR模式**: 启用VR渲染,将画面输出到VR头显
|
||||
- **退出VR模式**: 退出VR模式,回到桌面显示
|
||||
- **VR状态**: 查看VR系统状态和设备信息
|
||||
- **VR设置**: 配置VR渲染参数和性能选项
|
||||
|
||||
### VR交互
|
||||
|
||||
目前支持的VR交互功能:
|
||||
- **头部追踪**: 自动追踪头显位置和朝向
|
||||
- **房间规模移动**: 在设定的VR空间内移动
|
||||
- **立体显示**: 为左右眼提供不同视角的立体图像
|
||||
|
||||
## 配置选项
|
||||
|
||||
### VR设置对话框
|
||||
|
||||
在 `VR` → `VR设置` 中可以配置:
|
||||
|
||||
- **渲染质量**: 调整VR渲染的画质等级
|
||||
- **抗锯齿**: 设置抗锯齿级别
|
||||
- **刷新率**: 配置VR头显刷新率
|
||||
- **异步重投影**: 启用/禁用ATW技术
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
1. **降低渲染分辨率**: 如果性能不足,可以降低VR渲染质量
|
||||
2. **减少抗锯齿**: 关闭或降低抗锯齿级别
|
||||
3. **优化场景复杂度**: 减少场景中的多边形数量
|
||||
4. **关闭不必要的特效**: 暂时关闭高级渲染特效
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: 无法进入VR模式,提示"VR系统不可用"**
|
||||
A: 请检查:
|
||||
- SteamVR是否正在运行
|
||||
- VR头显是否正确连接
|
||||
- OpenVR库是否已安装 (`pip install openvr`)
|
||||
|
||||
**Q: VR画面卡顿或延迟**
|
||||
A: 尝试:
|
||||
- 降低VR渲染质量
|
||||
- 关闭其他占用GPU的程序
|
||||
- 检查VR头显连接线是否松动
|
||||
|
||||
**Q: VR菜单显示为灰色(不可用)**
|
||||
A: 检查:
|
||||
- VR管理器是否正确初始化
|
||||
- 查看控制台错误信息
|
||||
- 运行 `python vr_test.py` 进行诊断
|
||||
|
||||
**Q: 只能看到一只眼的画面**
|
||||
A: 这可能是:
|
||||
- VR渲染缓冲区创建失败
|
||||
- OpenVR投影矩阵设置错误
|
||||
- 检查图形驱动程序是否最新
|
||||
|
||||
### 诊断工具
|
||||
|
||||
使用内置的VR测试工具进行问题诊断:
|
||||
```bash
|
||||
python vr_test.py
|
||||
```
|
||||
|
||||
该工具会检测:
|
||||
- 基础环境和依赖
|
||||
- VR管理器状态
|
||||
- VR设备可用性
|
||||
- VR初始化过程
|
||||
- UI菜单集成
|
||||
|
||||
### 日志信息
|
||||
|
||||
VR相关的日志信息会输出到控制台,包括:
|
||||
- VR系统初始化状态
|
||||
- 渲染缓冲区创建信息
|
||||
- 追踪数据更新
|
||||
- 错误和警告信息
|
||||
|
||||
## 开发者信息
|
||||
|
||||
### VR架构
|
||||
|
||||
```
|
||||
main.py (MyWorld)
|
||||
├── core/vr_manager.py (VRManager)
|
||||
│ ├── OpenVR集成
|
||||
│ ├── 渲染缓冲区管理
|
||||
│ ├── 相机控制
|
||||
│ └── 追踪数据处理
|
||||
└── ui/main_window.py
|
||||
├── VR菜单
|
||||
├── VR设置对话框
|
||||
└── VR事件处理
|
||||
```
|
||||
|
||||
### 核心类
|
||||
|
||||
- **VRManager**: VR功能的核心管理类
|
||||
- **MainWindow**: UI集成和事件处理
|
||||
- **MyWorld**: VR管理器的宿主类
|
||||
|
||||
### 扩展开发
|
||||
|
||||
如需添加新的VR功能,可以:
|
||||
|
||||
1. 在 `VRManager` 中添加新方法
|
||||
2. 在 `MainWindow` 中添加对应的UI控件
|
||||
3. 更新VR设置对话框
|
||||
4. 添加相应的测试代码
|
||||
|
||||
## 技术支持
|
||||
|
||||
如果遇到问题,请:
|
||||
|
||||
1. 运行 `python vr_test.py` 获取诊断信息
|
||||
2. 检查控制台日志
|
||||
3. 确认SteamVR和硬件设置正确
|
||||
4. 提交问题时请包含详细的错误信息和系统配置
|
||||
|
||||
## 版本信息
|
||||
|
||||
- VR功能版本: 1.0.0
|
||||
- 支持的OpenVR版本: 2.2.0+
|
||||
- 兼容的SteamVR版本: 最新版本
|
||||
|
||||
---
|
||||
|
||||
**注意**: VR功能目前处于初始版本,可能还有一些限制和待完善的功能。我们会持续改进和优化VR体验。
|
||||
@ -1,6 +0,0 @@
|
||||
# Autogenerated
|
||||
name = 'Plastic-R0.0'
|
||||
roughness = 0.0
|
||||
ior = 1.51
|
||||
basecolor = (1, 0, 0)
|
||||
mat_type = 'default'
|
||||
583
core/vr_actions.py
Normal file
583
core/vr_actions.py
Normal file
@ -0,0 +1,583 @@
|
||||
"""
|
||||
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动作管理器已清理")
|
||||
282
core/vr_controller.py
Normal file
282
core/vr_controller.py
Normal file
@ -0,0 +1,282 @@
|
||||
"""
|
||||
VR手柄管理模块
|
||||
|
||||
基于panda3d-openvr参考实现,提供完整的VR手柄追踪和交互功能:
|
||||
- 手柄位置和姿态追踪
|
||||
- 按钮和触摸板输入处理
|
||||
- 手柄可视化和射线显示
|
||||
- 震动反馈支持
|
||||
"""
|
||||
|
||||
from panda3d.core import (
|
||||
NodePath, PandaNode, Vec3, Mat4, LVector3, LMatrix4,
|
||||
GeomNode, LineSegs, CardMaker, Texture, RenderState,
|
||||
TransparencyAttrib, ColorAttrib, Vec4
|
||||
)
|
||||
from direct.actor.Actor import Actor
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
|
||||
try:
|
||||
import openvr
|
||||
OPENVR_AVAILABLE = True
|
||||
except ImportError:
|
||||
OPENVR_AVAILABLE = False
|
||||
|
||||
# 导入可视化器
|
||||
from .vr_visualization import VRControllerVisualizer
|
||||
|
||||
|
||||
class VRController(DirectObject):
|
||||
"""VR手柄基类 - 管理单个手柄的追踪和交互"""
|
||||
|
||||
def __init__(self, vr_manager, name, hand_path, device_index=None):
|
||||
"""初始化VR手柄
|
||||
|
||||
Args:
|
||||
vr_manager: VR管理器实例
|
||||
name: 手柄名称 ('left' 或 'right')
|
||||
hand_path: OpenVR手部路径 ('/user/hand/left' 或 '/user/hand/right')
|
||||
device_index: OpenVR设备索引(可选)
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.vr_manager = vr_manager
|
||||
self.name = name
|
||||
self.hand_path = hand_path
|
||||
self.device_index = device_index
|
||||
|
||||
# 手柄状态
|
||||
self.is_connected = False
|
||||
self.is_pose_valid = False
|
||||
self.pose = Mat4.identMat()
|
||||
self.velocity = Vec3(0, 0, 0)
|
||||
self.angular_velocity = Vec3(0, 0, 0)
|
||||
|
||||
# 按钮状态
|
||||
self.button_states = {}
|
||||
self.previous_button_states = {}
|
||||
self.trigger_value = 0.0
|
||||
self.grip_value = 0.0
|
||||
self.touchpad_pos = Vec3(0, 0, 0)
|
||||
self.touchpad_touched = False
|
||||
|
||||
# 3D节点和可视化
|
||||
self.anchor_node = None
|
||||
self.visualizer = None
|
||||
self.ray_length = 10.0
|
||||
|
||||
# 初始化
|
||||
self._create_anchor()
|
||||
self._create_visualizer()
|
||||
|
||||
print(f"✓ {name}手柄控制器初始化完成")
|
||||
|
||||
def _create_anchor(self):
|
||||
"""创建手柄锚点节点"""
|
||||
if self.vr_manager.tracking_space:
|
||||
self.anchor_node = self.vr_manager.tracking_space.attachNewNode(f'{self.name}-controller')
|
||||
self.anchor_node.hide() # 初始隐藏,直到获得有效姿态
|
||||
|
||||
def _create_visualizer(self):
|
||||
"""创建手柄可视化器"""
|
||||
if self.anchor_node and hasattr(self.vr_manager, 'world'):
|
||||
self.visualizer = VRControllerVisualizer(self, self.vr_manager.world.render)
|
||||
elif self.anchor_node:
|
||||
# 如果没有世界对象,使用基础渲染节点
|
||||
from panda3d.core import NodePath
|
||||
render = NodePath('render')
|
||||
self.visualizer = VRControllerVisualizer(self, render)
|
||||
|
||||
def set_device_index(self, device_index):
|
||||
"""设置OpenVR设备索引"""
|
||||
self.device_index = device_index
|
||||
self.is_connected = True
|
||||
print(f"📱 {self.name}手柄连接 (设备索引: {device_index})")
|
||||
|
||||
def update_pose(self, pose_data):
|
||||
"""更新手柄姿态
|
||||
|
||||
Args:
|
||||
pose_data: OpenVR TrackedDevicePose_t数据
|
||||
"""
|
||||
if not pose_data.bPoseIsValid:
|
||||
self.is_pose_valid = False
|
||||
if self.anchor_node:
|
||||
self.anchor_node.hide()
|
||||
return
|
||||
|
||||
self.is_pose_valid = True
|
||||
|
||||
# 转换OpenVR矩阵到Panda3D
|
||||
if hasattr(self.vr_manager, 'convert_mat') and hasattr(self.vr_manager, 'coord_mat_inv') and hasattr(self.vr_manager, 'coord_mat'):
|
||||
modelview = self.vr_manager.convert_mat(pose_data.mDeviceToAbsoluteTracking)
|
||||
self.pose = self.vr_manager.coord_mat_inv * modelview * self.vr_manager.coord_mat
|
||||
else:
|
||||
# 直接使用矩阵数据
|
||||
m = pose_data.mDeviceToAbsoluteTracking.m
|
||||
self.pose = LMatrix4(
|
||||
m[0][0], m[1][0], m[2][0], m[3][0],
|
||||
m[0][1], m[1][1], m[2][1], m[3][1],
|
||||
m[0][2], m[1][2], m[2][2], m[3][2],
|
||||
m[0][3], m[1][3], m[2][3], m[3][3]
|
||||
)
|
||||
|
||||
# 更新锚点变换
|
||||
if self.anchor_node:
|
||||
self.anchor_node.setMat(self.pose)
|
||||
self.anchor_node.show()
|
||||
|
||||
# 更新可视化
|
||||
if self.visualizer:
|
||||
self.visualizer.update()
|
||||
|
||||
# 更新速度信息
|
||||
vel = pose_data.vVelocity
|
||||
self.velocity = Vec3(vel[0], vel[1], vel[2])
|
||||
|
||||
ang_vel = pose_data.vAngularVelocity
|
||||
self.angular_velocity = Vec3(ang_vel[0], ang_vel[1], ang_vel[2])
|
||||
|
||||
def update_input_state(self, vr_system):
|
||||
"""更新输入状态
|
||||
|
||||
Args:
|
||||
vr_system: OpenVR系统实例
|
||||
"""
|
||||
if not self.is_connected or not OPENVR_AVAILABLE or not vr_system:
|
||||
return
|
||||
|
||||
# 保存上一帧的按钮状态
|
||||
self.previous_button_states = self.button_states.copy()
|
||||
|
||||
# 获取控制器状态
|
||||
try:
|
||||
result, state = vr_system.getControllerState(self.device_index)
|
||||
if result:
|
||||
# 更新按钮状态
|
||||
for i in range(openvr.k_EButton_Max):
|
||||
button_mask = 1 << i
|
||||
self.button_states[i] = (state.rButtonPressed & button_mask) != 0
|
||||
|
||||
# 更新轴状态(扳机、握把、触摸板)
|
||||
if len(state.rAxis) > 0:
|
||||
# 扳机轴通常在axis[1].x
|
||||
if len(state.rAxis) > 1:
|
||||
self.trigger_value = state.rAxis[1].x
|
||||
|
||||
# 触摸板轴通常在axis[0]
|
||||
if len(state.rAxis) > 0:
|
||||
self.touchpad_pos = Vec3(state.rAxis[0].x, state.rAxis[0].y, 0)
|
||||
|
||||
# 触摸板触摸状态
|
||||
self.touchpad_touched = (state.rButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}")
|
||||
|
||||
def is_button_pressed(self, button_id):
|
||||
"""检查按钮是否被按下"""
|
||||
return self.button_states.get(button_id, False)
|
||||
|
||||
def is_button_just_pressed(self, button_id):
|
||||
"""检查按钮是否刚刚被按下(上升沿)"""
|
||||
current = self.button_states.get(button_id, False)
|
||||
previous = self.previous_button_states.get(button_id, False)
|
||||
return current and not previous
|
||||
|
||||
def is_button_just_released(self, button_id):
|
||||
"""检查按钮是否刚刚被释放(下降沿)"""
|
||||
current = self.button_states.get(button_id, False)
|
||||
previous = self.previous_button_states.get(button_id, False)
|
||||
return not current and previous
|
||||
|
||||
def is_trigger_pressed(self, threshold=0.1):
|
||||
"""检查扳机是否被按下"""
|
||||
return self.trigger_value > threshold
|
||||
|
||||
def is_grip_pressed(self, threshold=0.1):
|
||||
"""检查握把是否被按下"""
|
||||
return self.grip_value > threshold
|
||||
|
||||
def show_ray(self, show=True):
|
||||
"""显示或隐藏交互射线"""
|
||||
if self.visualizer:
|
||||
if show:
|
||||
self.visualizer.show_ray()
|
||||
else:
|
||||
self.visualizer.hide_ray()
|
||||
|
||||
def set_ray_color(self, color):
|
||||
"""设置射线颜色"""
|
||||
if self.visualizer and len(color) >= 3:
|
||||
from panda3d.core import Vec4
|
||||
color_vec = Vec4(color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
self.visualizer.set_ray_color(color_vec)
|
||||
|
||||
def trigger_haptic_feedback(self, duration=0.001, strength=1.0):
|
||||
"""触发震动反馈
|
||||
|
||||
Args:
|
||||
duration: 震动持续时间(秒)
|
||||
strength: 震动强度 (0.0-1.0)
|
||||
"""
|
||||
if not self.is_connected or not OPENVR_AVAILABLE:
|
||||
return
|
||||
|
||||
try:
|
||||
if hasattr(self.vr_manager, 'vr_system') and self.vr_manager.vr_system:
|
||||
# OpenVR的震动API
|
||||
duration_microseconds = int(duration * 1000000)
|
||||
self.vr_manager.vr_system.triggerHapticPulse(
|
||||
self.device_index,
|
||||
0, # axis ID (通常为0)
|
||||
int(strength * 3999) # 强度 (0-3999)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"⚠️ {self.name}手柄震动反馈失败: {e}")
|
||||
|
||||
def get_world_position(self):
|
||||
"""获取手柄在世界坐标系中的位置"""
|
||||
if self.anchor_node:
|
||||
return self.anchor_node.getPos(self.vr_manager.world.render)
|
||||
return Vec3(0, 0, 0)
|
||||
|
||||
def get_world_rotation(self):
|
||||
"""获取手柄在世界坐标系中的旋转"""
|
||||
if self.anchor_node:
|
||||
return self.anchor_node.getHpr(self.vr_manager.world.render)
|
||||
return Vec3(0, 0, 0)
|
||||
|
||||
def get_forward_direction(self):
|
||||
"""获取手柄指向的方向向量"""
|
||||
if self.anchor_node:
|
||||
# Y轴正方向为前方
|
||||
return self.anchor_node.getMat().getRow3(1).getXyz().normalized()
|
||||
return Vec3(0, 1, 0)
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
self.ignoreAll()
|
||||
|
||||
if self.visualizer:
|
||||
self.visualizer.cleanup()
|
||||
|
||||
if self.anchor_node:
|
||||
self.anchor_node.removeNode()
|
||||
|
||||
self.is_connected = False
|
||||
print(f"🧹 {self.name}手柄控制器已清理")
|
||||
|
||||
|
||||
class LeftController(VRController):
|
||||
"""左手控制器"""
|
||||
|
||||
def __init__(self, vr_manager):
|
||||
super().__init__(vr_manager, 'left', '/user/hand/left')
|
||||
|
||||
|
||||
class RightController(VRController):
|
||||
"""右手控制器"""
|
||||
|
||||
def __init__(self, vr_manager):
|
||||
super().__init__(vr_manager, 'right', '/user/hand/right')
|
||||
432
core/vr_interaction.py
Normal file
432
core/vr_interaction.py
Normal file
@ -0,0 +1,432 @@
|
||||
"""
|
||||
VR交互系统模块
|
||||
|
||||
提供VR手柄与3D场景的交互功能:
|
||||
- 射线投射和碰撞检测
|
||||
- 对象选择和高亮
|
||||
- 对象抓取和移动
|
||||
- UI交互
|
||||
- 距离抓取
|
||||
"""
|
||||
|
||||
from panda3d.core import (
|
||||
Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser,
|
||||
CollisionNode, CollisionHandlerQueue, BitMask32, NodePath,
|
||||
CollisionSphere, CollisionTube, RenderState, TransparencyAttrib,
|
||||
ColorAttrib
|
||||
)
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
|
||||
|
||||
class VRInteractionManager(DirectObject):
|
||||
"""VR交互管理器 - 处理手柄与场景的交互"""
|
||||
|
||||
def __init__(self, vr_manager):
|
||||
"""初始化VR交互管理器
|
||||
|
||||
Args:
|
||||
vr_manager: VR管理器实例
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.vr_manager = vr_manager
|
||||
self.world = vr_manager.world if hasattr(vr_manager, 'world') else None
|
||||
|
||||
# 碰撞检测系统
|
||||
self.collision_traverser = CollisionTraverser()
|
||||
self.collision_queue = CollisionHandlerQueue()
|
||||
|
||||
# 射线投射节点
|
||||
self.left_ray_node = None
|
||||
self.right_ray_node = None
|
||||
self.ray_collision_nodes = {}
|
||||
|
||||
# 选择和抓取状态
|
||||
self.selected_objects = {} # 控制器 -> 选中对象
|
||||
self.grabbed_objects = {} # 控制器 -> 抓取对象
|
||||
self.grab_offsets = {} # 控制器 -> 抓取偏移
|
||||
|
||||
# 交互参数
|
||||
self.selection_range = 50.0 # 选择距离
|
||||
self.grab_threshold = 0.5 # 抓取扳机阈值
|
||||
self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色
|
||||
self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色
|
||||
|
||||
# 高亮状态
|
||||
self.highlighted_objects = set()
|
||||
self.original_colors = {} # 存储对象原始颜色
|
||||
|
||||
print("✓ VR交互管理器初始化完成")
|
||||
|
||||
def initialize(self):
|
||||
"""初始化交互系统"""
|
||||
try:
|
||||
print("🔧 正在初始化VR交互系统...")
|
||||
|
||||
# 创建射线投射节点
|
||||
self._create_ray_casters()
|
||||
|
||||
# 设置碰撞检测
|
||||
self._setup_collision_detection()
|
||||
|
||||
print("✅ VR交互系统初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ VR交互系统初始化失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _create_ray_casters(self):
|
||||
"""创建射线投射节点"""
|
||||
# 为左手控制器创建射线
|
||||
if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node:
|
||||
self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node)
|
||||
|
||||
# 为右手控制器创建射线
|
||||
if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node:
|
||||
self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node)
|
||||
|
||||
def _create_controller_ray(self, controller_name, anchor_node):
|
||||
"""为控制器创建射线投射节点"""
|
||||
# 创建射线碰撞体
|
||||
ray = CollisionRay()
|
||||
ray.setOrigin(0, 0, 0) # 从控制器原点开始
|
||||
ray.setDirection(0, 1, 0) # 沿Y轴正方向
|
||||
|
||||
# 创建碰撞节点
|
||||
ray_collision_node = CollisionNode(f'{controller_name}_ray')
|
||||
ray_collision_node.addSolid(ray)
|
||||
|
||||
# 设置碰撞掩码
|
||||
ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码
|
||||
ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞
|
||||
|
||||
# 附加到控制器锚点
|
||||
ray_node = anchor_node.attachNewNode(ray_collision_node)
|
||||
self.ray_collision_nodes[controller_name] = ray_collision_node
|
||||
|
||||
# 注册到碰撞遍历器
|
||||
self.collision_traverser.addCollider(ray_node, self.collision_queue)
|
||||
|
||||
print(f"✓ {controller_name}手控制器射线投射已创建")
|
||||
return ray_node
|
||||
|
||||
def _setup_collision_detection(self):
|
||||
"""设置碰撞检测系统"""
|
||||
if self.world:
|
||||
# 使用世界的碰撞系统
|
||||
if hasattr(self.world, 'render'):
|
||||
# 为所有可交互对象设置碰撞体
|
||||
self._setup_scene_collision_objects()
|
||||
else:
|
||||
print("⚠️ 无法访问世界对象,跳过场景碰撞设置")
|
||||
|
||||
def _setup_scene_collision_objects(self):
|
||||
"""为场景对象设置碰撞体"""
|
||||
if not self.world or not hasattr(self.world, 'render'):
|
||||
return
|
||||
|
||||
try:
|
||||
# 遍历场景中的所有节点,为它们添加碰撞体
|
||||
for node_path in self.world.render.findAllMatches("**/+GeomNode"):
|
||||
self._add_collision_to_object(node_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 设置场景碰撞对象失败: {e}")
|
||||
|
||||
def _add_collision_to_object(self, node_path):
|
||||
"""为对象添加碰撞体"""
|
||||
try:
|
||||
# 获取对象的边界框
|
||||
bounds = node_path.getBounds()
|
||||
if bounds.isEmpty():
|
||||
return
|
||||
|
||||
# 计算边界球
|
||||
center = bounds.getCenter()
|
||||
radius = bounds.getRadius()
|
||||
|
||||
# 创建球形碰撞体
|
||||
collision_sphere = CollisionSphere(center, radius)
|
||||
|
||||
# 创建碰撞节点
|
||||
collision_node = CollisionNode(f'{node_path.getName()}_collision')
|
||||
collision_node.addSolid(collision_sphere)
|
||||
|
||||
# 设置碰撞掩码
|
||||
collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞
|
||||
collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线
|
||||
|
||||
# 附加碰撞节点
|
||||
collision_node_path = node_path.attachNewNode(collision_node)
|
||||
|
||||
# 标记为可交互对象
|
||||
node_path.setTag('interactable', 'true')
|
||||
node_path.setTag('original_name', node_path.getName())
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}")
|
||||
|
||||
def update(self):
|
||||
"""更新交互系统 - 每帧调用"""
|
||||
if not self.vr_manager.are_controllers_connected():
|
||||
return
|
||||
|
||||
# 执行碰撞检测
|
||||
self._perform_collision_detection()
|
||||
|
||||
# 更新选择状态
|
||||
self._update_selections()
|
||||
|
||||
# 更新抓取状态
|
||||
self._update_grabbing()
|
||||
|
||||
def _perform_collision_detection(self):
|
||||
"""执行碰撞检测"""
|
||||
if self.world and hasattr(self.world, 'render'):
|
||||
self.collision_traverser.traverse(self.world.render)
|
||||
|
||||
def _update_selections(self):
|
||||
"""更新对象选择状态"""
|
||||
# 清除之前的选择高亮
|
||||
self._clear_selection_highlights()
|
||||
|
||||
# 检查每个控制器的选择
|
||||
for controller in self.vr_manager.get_connected_controllers():
|
||||
if not controller:
|
||||
continue
|
||||
|
||||
# 获取最近的碰撞对象
|
||||
hit_object = self._get_closest_hit_object(controller.name)
|
||||
|
||||
if hit_object:
|
||||
# 高亮选中的对象
|
||||
self._highlight_object(hit_object, self.selection_color)
|
||||
self.selected_objects[controller.name] = hit_object
|
||||
|
||||
# 显示控制器射线
|
||||
controller.show_ray(True)
|
||||
controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色
|
||||
else:
|
||||
# 没有选中对象
|
||||
if controller.name in self.selected_objects:
|
||||
del self.selected_objects[controller.name]
|
||||
|
||||
# 隐藏射线(除非正在抓取)
|
||||
if controller.name not in self.grabbed_objects:
|
||||
controller.show_ray(False)
|
||||
|
||||
def _get_closest_hit_object(self, controller_name):
|
||||
"""获取指定控制器射线最近的碰撞对象"""
|
||||
if controller_name not in self.ray_collision_nodes:
|
||||
return None
|
||||
|
||||
closest_object = None
|
||||
closest_distance = float('inf')
|
||||
|
||||
# 检查碰撞队列中的条目
|
||||
for i in range(self.collision_queue.getNumEntries()):
|
||||
entry = self.collision_queue.getEntry(i)
|
||||
|
||||
# 检查是否是该控制器的射线
|
||||
from_node = entry.getFromNodePath()
|
||||
if from_node.node() == self.ray_collision_nodes[controller_name]:
|
||||
# 获取碰撞的对象
|
||||
hit_node_path = entry.getIntoNodePath()
|
||||
|
||||
# 获取实际的几何对象(父节点)
|
||||
geom_object = hit_node_path.getParent()
|
||||
if geom_object and geom_object.hasTag('interactable'):
|
||||
distance = entry.getSurfacePoint(geom_object).length()
|
||||
|
||||
if distance < closest_distance and distance <= self.selection_range:
|
||||
closest_distance = distance
|
||||
closest_object = geom_object
|
||||
|
||||
return closest_object
|
||||
|
||||
def _update_grabbing(self):
|
||||
"""更新对象抓取状态"""
|
||||
for controller in self.vr_manager.get_connected_controllers():
|
||||
if not controller:
|
||||
continue
|
||||
|
||||
controller_name = controller.name
|
||||
|
||||
# 检查是否按下抓取按钮
|
||||
if controller.is_trigger_pressed(threshold=self.grab_threshold):
|
||||
# 如果还没有抓取对象
|
||||
if controller_name not in self.grabbed_objects:
|
||||
# 尝试抓取选中的对象
|
||||
if controller_name in self.selected_objects:
|
||||
selected_obj = self.selected_objects[controller_name]
|
||||
self._start_grab(controller, selected_obj)
|
||||
|
||||
# 如果正在抓取,更新对象位置
|
||||
if controller_name in self.grabbed_objects:
|
||||
self._update_grabbed_object(controller)
|
||||
|
||||
else:
|
||||
# 释放抓取
|
||||
if controller_name in self.grabbed_objects:
|
||||
self._release_grab(controller)
|
||||
|
||||
def _start_grab(self, controller, obj):
|
||||
"""开始抓取对象"""
|
||||
controller_name = controller.name
|
||||
|
||||
try:
|
||||
# 计算抓取偏移(对象相对于控制器的位置)
|
||||
controller_pos = controller.get_world_position()
|
||||
object_pos = obj.getPos(self.world.render if self.world else obj.getParent())
|
||||
|
||||
offset = object_pos - controller_pos
|
||||
self.grab_offsets[controller_name] = offset
|
||||
|
||||
# 记录抓取状态
|
||||
self.grabbed_objects[controller_name] = obj
|
||||
|
||||
# 改变对象颜色表示抓取状态
|
||||
self._highlight_object(obj, self.grab_color)
|
||||
|
||||
# 触发震动反馈
|
||||
controller.trigger_haptic_feedback(0.01, 0.8)
|
||||
|
||||
# 显示绿色射线表示抓取
|
||||
controller.show_ray(True)
|
||||
controller.set_ray_color([0.2, 0.9, 0.2, 0.8])
|
||||
|
||||
print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 开始抓取失败: {e}")
|
||||
|
||||
def _update_grabbed_object(self, controller):
|
||||
"""更新被抓取对象的位置"""
|
||||
controller_name = controller.name
|
||||
|
||||
if controller_name not in self.grabbed_objects:
|
||||
return
|
||||
|
||||
try:
|
||||
grabbed_obj = self.grabbed_objects[controller_name]
|
||||
grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0))
|
||||
|
||||
# 计算新位置
|
||||
controller_pos = controller.get_world_position()
|
||||
new_pos = controller_pos + grab_offset
|
||||
|
||||
# 更新对象位置
|
||||
grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos)
|
||||
|
||||
# 可选:同步旋转
|
||||
if hasattr(controller, 'get_world_rotation'):
|
||||
controller_rot = controller.get_world_rotation()
|
||||
grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 更新抓取对象失败: {e}")
|
||||
|
||||
def _release_grab(self, controller):
|
||||
"""释放抓取的对象"""
|
||||
controller_name = controller.name
|
||||
|
||||
if controller_name not in self.grabbed_objects:
|
||||
return
|
||||
|
||||
try:
|
||||
grabbed_obj = self.grabbed_objects[controller_name]
|
||||
|
||||
# 恢复对象原始颜色
|
||||
self._restore_object_color(grabbed_obj)
|
||||
|
||||
# 清理抓取状态
|
||||
del self.grabbed_objects[controller_name]
|
||||
if controller_name in self.grab_offsets:
|
||||
del self.grab_offsets[controller_name]
|
||||
|
||||
# 触发震动反馈
|
||||
controller.trigger_haptic_feedback(0.005, 0.4)
|
||||
|
||||
print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 释放抓取失败: {e}")
|
||||
|
||||
def _highlight_object(self, obj, color):
|
||||
"""高亮显示对象"""
|
||||
if obj in self.highlighted_objects:
|
||||
return
|
||||
|
||||
try:
|
||||
# 保存原始颜色
|
||||
if obj not in self.original_colors:
|
||||
self.original_colors[obj] = obj.getColor()
|
||||
|
||||
# 设置高亮颜色
|
||||
obj.setColor(color)
|
||||
self.highlighted_objects.add(obj)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 高亮对象失败: {e}")
|
||||
|
||||
def _restore_object_color(self, obj):
|
||||
"""恢复对象原始颜色"""
|
||||
if obj not in self.highlighted_objects:
|
||||
return
|
||||
|
||||
try:
|
||||
# 恢复原始颜色
|
||||
if obj in self.original_colors:
|
||||
obj.setColor(self.original_colors[obj])
|
||||
del self.original_colors[obj]
|
||||
|
||||
self.highlighted_objects.discard(obj)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 恢复对象颜色失败: {e}")
|
||||
|
||||
def _clear_selection_highlights(self):
|
||||
"""清除所有选择高亮"""
|
||||
for obj in list(self.highlighted_objects):
|
||||
# 只清除非抓取状态的对象
|
||||
is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values())
|
||||
if not is_grabbed:
|
||||
self._restore_object_color(obj)
|
||||
|
||||
def get_selected_object(self, controller_name):
|
||||
"""获取指定控制器选中的对象"""
|
||||
return self.selected_objects.get(controller_name)
|
||||
|
||||
def get_grabbed_object(self, controller_name):
|
||||
"""获取指定控制器抓取的对象"""
|
||||
return self.grabbed_objects.get(controller_name)
|
||||
|
||||
def is_grabbing(self, controller_name):
|
||||
"""检查指定控制器是否正在抓取对象"""
|
||||
return controller_name in self.grabbed_objects
|
||||
|
||||
def force_release_all(self):
|
||||
"""强制释放所有抓取的对象"""
|
||||
for controller in self.vr_manager.get_connected_controllers():
|
||||
if controller and controller.name in self.grabbed_objects:
|
||||
self._release_grab(controller)
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
self.ignoreAll()
|
||||
|
||||
# 释放所有抓取
|
||||
self.force_release_all()
|
||||
|
||||
# 清理碰撞系统
|
||||
self.collision_traverser.clearColliders()
|
||||
self.ray_collision_nodes.clear()
|
||||
|
||||
# 清理高亮状态
|
||||
for obj in list(self.highlighted_objects):
|
||||
self._restore_object_color(obj)
|
||||
|
||||
print("🧹 VR交互管理器已清理")
|
||||
@ -27,6 +27,11 @@ except ImportError:
|
||||
OPENVR_AVAILABLE = False
|
||||
print("警告: OpenVR未安装,VR功能将不可用")
|
||||
|
||||
# 导入手柄控制器、动作系统和交互系统
|
||||
from .vr_controller import LeftController, RightController
|
||||
from .vr_actions import VRActionManager
|
||||
from .vr_interaction import VRInteractionManager
|
||||
|
||||
|
||||
class VRManager(DirectObject):
|
||||
"""VR管理器类 - 处理所有VR相关功能"""
|
||||
@ -87,6 +92,18 @@ class VRManager(DirectObject):
|
||||
# VR提交策略 - 基于参考实现
|
||||
self.submit_together = True # 是否在right_cb中同时提交左右眼
|
||||
|
||||
# VR手柄控制器
|
||||
self.left_controller = None
|
||||
self.right_controller = None
|
||||
self.controllers = {} # 设备索引到控制器的映射
|
||||
self.tracked_device_anchors = {} # 跟踪设备锚点
|
||||
|
||||
# VR动作系统
|
||||
self.action_manager = VRActionManager(self)
|
||||
|
||||
# VR交互系统
|
||||
self.interaction_manager = VRInteractionManager(self)
|
||||
|
||||
print("✓ VR管理器初始化完成")
|
||||
|
||||
def convert_mat(self, mat):
|
||||
@ -163,6 +180,17 @@ class VRManager(DirectObject):
|
||||
print("❌ 设置VR相机失败")
|
||||
return False
|
||||
|
||||
# 初始化手柄控制器
|
||||
self._initialize_controllers()
|
||||
|
||||
# 初始化动作系统
|
||||
if not self.action_manager.initialize():
|
||||
print("⚠️ VR动作系统初始化失败,但VR系统将继续运行")
|
||||
|
||||
# 初始化交互系统
|
||||
if not self.interaction_manager.initialize():
|
||||
print("⚠️ VR交互系统初始化失败,但VR系统将继续运行")
|
||||
|
||||
# 启动VR更新任务
|
||||
self._start_vr_task()
|
||||
|
||||
@ -368,6 +396,15 @@ class VRManager(DirectObject):
|
||||
# 2. 更新相机位置(使用刚获取的最新姿态数据)
|
||||
self._update_camera_poses()
|
||||
|
||||
# 3. 更新手柄和其他跟踪设备
|
||||
self.update_tracked_devices()
|
||||
|
||||
# 4. 更新VR动作状态
|
||||
self.action_manager.update_actions()
|
||||
|
||||
# 5. 更新VR交互系统
|
||||
self.interaction_manager.update()
|
||||
|
||||
# 注意:纹理提交现在通过渲染回调自动处理
|
||||
|
||||
# 定期输出性能报告
|
||||
@ -929,4 +966,314 @@ class VRManager(DirectObject):
|
||||
|
||||
print("✓ 主相机已恢复")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 恢复主相机失败: {e}")
|
||||
print(f"⚠️ 恢复主相机失败: {e}")
|
||||
|
||||
def _initialize_controllers(self):
|
||||
"""初始化VR手柄控制器"""
|
||||
try:
|
||||
print("🎮 正在初始化VR手柄控制器...")
|
||||
|
||||
# 创建左右手柄控制器实例
|
||||
self.left_controller = LeftController(self)
|
||||
self.right_controller = RightController(self)
|
||||
|
||||
# 检测现有连接的控制器
|
||||
self._detect_controllers()
|
||||
|
||||
print("✓ VR手柄控制器初始化完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ VR手柄初始化失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _detect_controllers(self):
|
||||
"""检测并连接VR控制器"""
|
||||
if not self.vr_system:
|
||||
return
|
||||
|
||||
try:
|
||||
for device_index in range(openvr.k_unMaxTrackedDeviceCount):
|
||||
# 检查设备是否已连接
|
||||
if not self.vr_system.isTrackedDeviceConnected(device_index):
|
||||
continue
|
||||
|
||||
# 获取设备类型
|
||||
device_class = self.vr_system.getTrackedDeviceClass(device_index)
|
||||
|
||||
if device_class == openvr.TrackedDeviceClass_Controller:
|
||||
# 获取控制器角色
|
||||
role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index)
|
||||
|
||||
if role == openvr.TrackedControllerRole_LeftHand and self.left_controller:
|
||||
self.left_controller.set_device_index(device_index)
|
||||
self.controllers[device_index] = self.left_controller
|
||||
# 为设备创建锚点
|
||||
self._create_tracked_device_anchor(device_index, 'left_controller')
|
||||
|
||||
elif role == openvr.TrackedControllerRole_RightHand and self.right_controller:
|
||||
self.right_controller.set_device_index(device_index)
|
||||
self.controllers[device_index] = self.right_controller
|
||||
# 为设备创建锚点
|
||||
self._create_tracked_device_anchor(device_index, 'right_controller')
|
||||
|
||||
print(f"🎮 检测到 {len(self.controllers)} 个控制器")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 控制器检测失败: {e}")
|
||||
|
||||
def _create_tracked_device_anchor(self, device_index, name):
|
||||
"""为跟踪设备创建锚点节点"""
|
||||
if not self.tracking_space:
|
||||
print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化")
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取设备模型名称
|
||||
if self.vr_system:
|
||||
model_name = self.vr_system.getStringTrackedDeviceProperty(
|
||||
device_index,
|
||||
openvr.Prop_RenderModelName_String
|
||||
)
|
||||
anchor_name = f"{device_index}:{model_name}:{name}"
|
||||
else:
|
||||
anchor_name = f"{device_index}:{name}"
|
||||
|
||||
# 创建锚点节点
|
||||
device_anchor = self.tracking_space.attachNewNode(anchor_name)
|
||||
self.tracked_device_anchors[device_index] = device_anchor
|
||||
|
||||
print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 创建设备锚点失败: {e}")
|
||||
|
||||
def update_tracked_devices(self):
|
||||
"""更新所有跟踪设备的姿态 - 基于参考实现"""
|
||||
if not self.poses or not self.vr_system:
|
||||
return
|
||||
|
||||
try:
|
||||
# 更新每个已连接的控制器
|
||||
for device_index, controller in self.controllers.items():
|
||||
if device_index < len(self.poses):
|
||||
pose_data = self.poses[device_index]
|
||||
|
||||
# 更新控制器姿态
|
||||
controller.update_pose(pose_data)
|
||||
|
||||
# 更新控制器输入状态
|
||||
controller.update_input_state(self.vr_system)
|
||||
|
||||
# 更新其他跟踪设备的锚点
|
||||
for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)):
|
||||
if device_index in self.tracked_device_anchors:
|
||||
pose_data = self.poses[device_index]
|
||||
if pose_data.bPoseIsValid:
|
||||
# 转换姿态矩阵
|
||||
modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking)
|
||||
final_matrix = self.coord_mat_inv * modelview * self.coord_mat
|
||||
|
||||
# 更新锚点变换
|
||||
anchor = self.tracked_device_anchors[device_index]
|
||||
anchor.setMat(final_matrix)
|
||||
anchor.show()
|
||||
else:
|
||||
# 姿态无效,隐藏锚点
|
||||
self.tracked_device_anchors[device_index].hide()
|
||||
|
||||
except Exception as e:
|
||||
if self.frame_count % 300 == 0: # 每5秒输出一次错误
|
||||
print(f"⚠️ 更新跟踪设备失败: {e}")
|
||||
|
||||
def get_controller_by_role(self, role):
|
||||
"""根据角色获取控制器
|
||||
|
||||
Args:
|
||||
role: 'left' 或 'right'
|
||||
|
||||
Returns:
|
||||
VRController实例或None
|
||||
"""
|
||||
if role == 'left':
|
||||
return self.left_controller
|
||||
elif role == 'right':
|
||||
return self.right_controller
|
||||
return None
|
||||
|
||||
def are_controllers_connected(self):
|
||||
"""检查是否有控制器连接"""
|
||||
return len(self.controllers) > 0
|
||||
|
||||
def get_connected_controllers(self):
|
||||
"""获取所有连接的控制器列表"""
|
||||
return list(self.controllers.values())
|
||||
|
||||
def trigger_controller_haptic(self, role, duration=0.001, strength=1.0):
|
||||
"""触发控制器震动反馈
|
||||
|
||||
Args:
|
||||
role: 'left', 'right' 或 'both'
|
||||
duration: 震动持续时间(秒)
|
||||
strength: 震动强度 (0.0-1.0)
|
||||
"""
|
||||
if role in ['left', 'both'] and self.left_controller:
|
||||
self.left_controller.trigger_haptic_feedback(duration, strength)
|
||||
|
||||
if role in ['right', 'both'] and self.right_controller:
|
||||
self.right_controller.trigger_haptic_feedback(duration, strength)
|
||||
|
||||
# VR动作系统便捷方法
|
||||
def is_trigger_pressed(self, hand='any'):
|
||||
"""检查扳机是否被按下
|
||||
|
||||
Args:
|
||||
hand: 'left', 'right', 'any'
|
||||
"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path)
|
||||
return pressed
|
||||
|
||||
def is_trigger_just_pressed(self, hand='any'):
|
||||
"""检查扳机是否刚刚被按下"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path)
|
||||
return pressed
|
||||
|
||||
def is_grip_pressed(self, hand='any'):
|
||||
"""检查握把是否被按下"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path)
|
||||
return pressed
|
||||
|
||||
def is_grip_just_pressed(self, hand='any'):
|
||||
"""检查握把是否刚刚被按下"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path)
|
||||
return pressed
|
||||
|
||||
def is_menu_pressed(self, hand='any'):
|
||||
"""检查菜单按钮是否被按下"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path)
|
||||
return pressed
|
||||
|
||||
def is_trackpad_touched(self, hand='any'):
|
||||
"""检查触摸板是否被触摸"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path)
|
||||
return touched
|
||||
|
||||
def get_trackpad_position(self, hand='any'):
|
||||
"""获取触摸板位置
|
||||
|
||||
Returns:
|
||||
Vec2或None: 触摸板位置 (-1到1的范围)
|
||||
"""
|
||||
device_path = None
|
||||
if hand == 'left':
|
||||
device_path = '/user/hand/left'
|
||||
elif hand == 'right':
|
||||
device_path = '/user/hand/right'
|
||||
|
||||
value, _ = self.action_manager.get_analog_action_value('trackpad', device_path)
|
||||
return value
|
||||
|
||||
# VR交互系统便捷方法
|
||||
def get_selected_object(self, hand='any'):
|
||||
"""获取指定手选中的对象
|
||||
|
||||
Args:
|
||||
hand: 'left', 'right', 'any'
|
||||
|
||||
Returns:
|
||||
选中的对象节点或None
|
||||
"""
|
||||
if hand == 'any':
|
||||
# 返回任意手选中的对象
|
||||
for controller in self.get_connected_controllers():
|
||||
selected = self.interaction_manager.get_selected_object(controller.name)
|
||||
if selected:
|
||||
return selected
|
||||
return None
|
||||
else:
|
||||
return self.interaction_manager.get_selected_object(hand)
|
||||
|
||||
def get_grabbed_object(self, hand='any'):
|
||||
"""获取指定手抓取的对象
|
||||
|
||||
Args:
|
||||
hand: 'left', 'right', 'any'
|
||||
|
||||
Returns:
|
||||
抓取的对象节点或None
|
||||
"""
|
||||
if hand == 'any':
|
||||
# 返回任意手抓取的对象
|
||||
for controller in self.get_connected_controllers():
|
||||
grabbed = self.interaction_manager.get_grabbed_object(controller.name)
|
||||
if grabbed:
|
||||
return grabbed
|
||||
return None
|
||||
else:
|
||||
return self.interaction_manager.get_grabbed_object(hand)
|
||||
|
||||
def is_grabbing_object(self, hand='any'):
|
||||
"""检查是否正在抓取对象
|
||||
|
||||
Args:
|
||||
hand: 'left', 'right', 'any'
|
||||
|
||||
Returns:
|
||||
bool: 是否正在抓取
|
||||
"""
|
||||
if hand == 'any':
|
||||
# 检查任意手是否正在抓取
|
||||
for controller in self.get_connected_controllers():
|
||||
if self.interaction_manager.is_grabbing(controller.name):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return self.interaction_manager.is_grabbing(hand)
|
||||
|
||||
def force_release_all_grabs(self):
|
||||
"""强制释放所有抓取的对象"""
|
||||
self.interaction_manager.force_release_all()
|
||||
|
||||
def add_interactable_object(self, object_node):
|
||||
"""将对象标记为可交互
|
||||
|
||||
Args:
|
||||
object_node: 要标记的对象节点
|
||||
"""
|
||||
self.interaction_manager._add_collision_to_object(object_node)
|
||||
658
core/vr_visualization.py
Normal file
658
core/vr_visualization.py
Normal file
@ -0,0 +1,658 @@
|
||||
"""
|
||||
VR可视化模块
|
||||
|
||||
提供VR手柄和交互元素的高级可视化功能:
|
||||
- 手柄3D模型渲染
|
||||
- 交互射线显示
|
||||
- 按钮状态可视化
|
||||
- 触摸板和扳机反馈
|
||||
"""
|
||||
|
||||
from panda3d.core import (
|
||||
NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData,
|
||||
GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints,
|
||||
Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib,
|
||||
InternalName, loadPrcFileData
|
||||
)
|
||||
from panda3d.core import Texture, Material, TextureStage
|
||||
|
||||
# 启用Assimp支持OBJ文件加载
|
||||
loadPrcFileData("", "load-file-type p3assimp")
|
||||
|
||||
|
||||
class VRControllerVisualizer:
|
||||
"""VR手柄可视化器"""
|
||||
|
||||
def __init__(self, controller, render_node):
|
||||
"""初始化手柄可视化器
|
||||
|
||||
Args:
|
||||
controller: VRController实例
|
||||
render_node: 渲染节点
|
||||
"""
|
||||
self.controller = controller
|
||||
self.render = render_node
|
||||
|
||||
# 可视化节点
|
||||
self.visual_node = None
|
||||
self.model_node = None
|
||||
self.ray_node = None
|
||||
self.button_indicator_node = None
|
||||
|
||||
# 射线参数
|
||||
self.ray_length = 10.0
|
||||
self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8)
|
||||
self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8)
|
||||
|
||||
# 按钮指示器参数
|
||||
self.button_colors = {
|
||||
'normal': Vec4(0.3, 0.3, 0.8, 1.0),
|
||||
'left': Vec4(0.2, 0.6, 0.9, 1.0),
|
||||
'right': Vec4(0.9, 0.3, 0.3, 1.0),
|
||||
'trigger': Vec4(0.9, 0.6, 0.2, 1.0),
|
||||
'grip': Vec4(0.6, 0.9, 0.3, 1.0),
|
||||
'menu': Vec4(0.9, 0.2, 0.9, 1.0),
|
||||
'trackpad': Vec4(0.2, 0.9, 0.9, 1.0)
|
||||
}
|
||||
|
||||
self._create_visual_components()
|
||||
|
||||
def _create_visual_components(self):
|
||||
"""创建可视化组件"""
|
||||
if not self.controller.anchor_node:
|
||||
return
|
||||
|
||||
# 创建主可视化节点
|
||||
self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual')
|
||||
|
||||
# 创建手柄模型
|
||||
self._create_controller_model()
|
||||
|
||||
# 创建交互射线
|
||||
self._create_interaction_ray()
|
||||
|
||||
# 暂时注释按钮指示器功能,避免额外几何体造成悬空零件
|
||||
# self._create_button_indicators()
|
||||
|
||||
def _create_controller_model(self):
|
||||
"""创建手柄3D模型"""
|
||||
if not self.visual_node:
|
||||
return
|
||||
|
||||
# 创建模型节点
|
||||
self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model')
|
||||
|
||||
# 尝试加载SteamVR官方模型
|
||||
steamvr_model = self._load_steamvr_model()
|
||||
|
||||
if steamvr_model:
|
||||
# 使用SteamVR官方模型
|
||||
steamvr_model.reparentTo(self.model_node)
|
||||
|
||||
# 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度
|
||||
# body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度
|
||||
# 右手正确,左手需要反向
|
||||
if self.controller.name == 'left':
|
||||
# 左手控制器:绕Y轴俯仰+90度(修正反向)
|
||||
steamvr_model.setHpr(0, 5.037 + 90, 0)
|
||||
else:
|
||||
# 右手控制器:绕Y轴俯仰+90度(保持不变)
|
||||
steamvr_model.setHpr(0, 5.037 + 90, 0)
|
||||
|
||||
# 设置合适的缩放值
|
||||
steamvr_model.setScale(1.0)
|
||||
|
||||
# 打印实际应用的旋转值
|
||||
if self.controller.name == 'left':
|
||||
print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]")
|
||||
else:
|
||||
print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]")
|
||||
|
||||
# 修复纯黑色问题:重新设置材质属性
|
||||
self._fix_model_material(steamvr_model)
|
||||
|
||||
# 暂时注释身份标记功能,避免额外几何体造成悬空零件
|
||||
# self._apply_controller_identity_marker(steamvr_model)
|
||||
|
||||
# 设置手柄始终显示在上层
|
||||
self._set_always_on_top(steamvr_model)
|
||||
|
||||
print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)")
|
||||
else:
|
||||
# 降级到改进的程序化模型
|
||||
self._create_fallback_model()
|
||||
print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)")
|
||||
|
||||
def _load_steamvr_model(self):
|
||||
"""加载SteamVR官方手柄模型"""
|
||||
import os
|
||||
from panda3d.core import Filename, Texture, TextureStage
|
||||
|
||||
# SteamVR模型基础路径
|
||||
steamvr_base_paths = [
|
||||
"/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5",
|
||||
"/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5",
|
||||
"~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5"
|
||||
]
|
||||
|
||||
for base_path in steamvr_base_paths:
|
||||
expanded_base_path = os.path.expanduser(base_path)
|
||||
if os.path.exists(expanded_base_path):
|
||||
print(f"🔍 找到SteamVR模型目录: {expanded_base_path}")
|
||||
|
||||
# 不再添加目录到搜索路径,避免自动加载多余组件
|
||||
# from panda3d.core import getModelPath
|
||||
# getModelPath().appendDirectory(expanded_base_path)
|
||||
|
||||
# 尝试加载不同的模型文件,按优先级排序
|
||||
model_files = [
|
||||
("body.obj", "手柄主体"), # 最重要的部分
|
||||
("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型
|
||||
]
|
||||
|
||||
for model_file, description in model_files:
|
||||
model_path = os.path.join(expanded_base_path, model_file)
|
||||
if os.path.exists(model_path):
|
||||
try:
|
||||
print(f"🎮 尝试加载{description}: {model_file}")
|
||||
|
||||
# 加载主模型
|
||||
model = loader.loadModel(Filename.fromOsSpecific(model_path))
|
||||
if model:
|
||||
# 先应用纹理,再修复材质(保持纹理效果)
|
||||
self._apply_steamvr_textures(model, expanded_base_path)
|
||||
print(f"✅ 成功加载{description}")
|
||||
return model
|
||||
else:
|
||||
print(f"⚠️ 模型文件存在但加载失败: {model_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载{description}失败: {e}")
|
||||
continue
|
||||
|
||||
# 不再尝试组合加载多个部件,避免悬空零件问题
|
||||
print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件")
|
||||
break
|
||||
|
||||
print("❌ 未找到任何SteamVR模型目录")
|
||||
return None
|
||||
|
||||
def _fix_model_material(self, model):
|
||||
"""修复模型材质,解决纯黑色问题同时保持纹理"""
|
||||
from panda3d.core import Material, MaterialAttrib
|
||||
|
||||
# 检查模型是否有纹理
|
||||
has_texture = model.hasTexture()
|
||||
|
||||
# 创建新的材质
|
||||
material = Material()
|
||||
|
||||
if has_texture:
|
||||
# 有纹理时,设置材质以增强纹理效果
|
||||
material.setDiffuse((1.0, 1.0, 1.0, 1.0)) # 白色漫反射让纹理完全显示
|
||||
material.setAmbient((0.4, 0.4, 0.4, 1.0)) # 适度环境光
|
||||
material.setSpecular((0.3, 0.3, 0.3, 1.0)) # 轻度高光
|
||||
material.setShininess(16.0) # 中等光泽度
|
||||
print(f"🎨 {self.controller.name}手柄:已修复材质(保持纹理效果)")
|
||||
else:
|
||||
# 无纹理时,设置合适的基础颜色
|
||||
material.setDiffuse((0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色
|
||||
material.setAmbient((0.3, 0.3, 0.3, 1.0)) # 环境光
|
||||
material.setSpecular((0.5, 0.5, 0.5, 1.0)) # 高光
|
||||
material.setShininess(32.0) # 光泽度
|
||||
print(f"🎨 {self.controller.name}手柄:已修复材质(使用颜色)")
|
||||
|
||||
# 应用材质到模型
|
||||
model.setMaterial(material)
|
||||
|
||||
# 确保模型能正确渲染
|
||||
model.setTwoSided(False)
|
||||
|
||||
def _apply_controller_identity_marker(self, model):
|
||||
"""为控制器添加身份标记,区分左右手"""
|
||||
from panda3d.core import RenderModeAttrib
|
||||
|
||||
# 创建一个小的标识几何体
|
||||
marker_geom = self._create_box_geometry(0.005, 0.005, 0.02)
|
||||
marker_node = model.attachNewNode(marker_geom)
|
||||
|
||||
# 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰)
|
||||
if self.controller.name == 'left':
|
||||
# 左手控制器:左侧标记
|
||||
marker_node.setPos(-0.03, 0.05, 0.02)
|
||||
marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色
|
||||
print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记")
|
||||
else:
|
||||
# 右手控制器:右侧标记
|
||||
marker_node.setPos(0.03, 0.05, 0.02)
|
||||
marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色
|
||||
print(f"🔴 {self.controller.name}手柄已添加红色身份标记")
|
||||
|
||||
# 让标记发光以便更容易看到
|
||||
marker_node.setLightOff() # 不受光照影响,保持明亮
|
||||
|
||||
# 添加轻微的色彩调整(非常微弱,不影响主要纹理)
|
||||
if self.controller.name == 'left':
|
||||
model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调
|
||||
else:
|
||||
model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调
|
||||
|
||||
|
||||
def _apply_steamvr_textures(self, model, base_path):
|
||||
"""为SteamVR模型应用纹理"""
|
||||
import os
|
||||
from panda3d.core import Texture, TextureStage
|
||||
|
||||
# SteamVR纹理文件
|
||||
texture_files = {
|
||||
'diffuse': 'onepointfive_texture.png',
|
||||
'specular': 'onepointfive_spec.png'
|
||||
}
|
||||
|
||||
textures_applied = 0
|
||||
|
||||
for texture_type, texture_file in texture_files.items():
|
||||
texture_path = os.path.join(base_path, texture_file)
|
||||
if os.path.exists(texture_path):
|
||||
try:
|
||||
texture = loader.loadTexture(texture_path)
|
||||
if texture:
|
||||
# 确保纹理能正确加载
|
||||
texture.setWrapU(Texture.WMClamp)
|
||||
texture.setWrapV(Texture.WMClamp)
|
||||
texture.setMinfilter(Texture.FTLinearMipmapLinear)
|
||||
texture.setMagfilter(Texture.FTLinear)
|
||||
|
||||
if texture_type == 'diffuse':
|
||||
# 应用主要漫反射纹理
|
||||
model.setTexture(texture)
|
||||
print(f"✅ 应用了主纹理: {texture_file}")
|
||||
textures_applied += 1
|
||||
elif texture_type == 'specular':
|
||||
# 应用高光纹理
|
||||
ts = TextureStage('specular')
|
||||
ts.setMode(TextureStage.MModulate)
|
||||
model.setTexture(ts, texture)
|
||||
print(f"✅ 应用了高光纹理: {texture_file}")
|
||||
textures_applied += 1
|
||||
except Exception as e:
|
||||
print(f"⚠️ 应用纹理失败 {texture_file}: {e}")
|
||||
|
||||
if textures_applied == 0:
|
||||
print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色")
|
||||
else:
|
||||
print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理")
|
||||
|
||||
def _load_combined_steamvr_model(self, base_path):
|
||||
"""尝试组合加载多个SteamVR模型部件"""
|
||||
import os
|
||||
from panda3d.core import NodePath
|
||||
|
||||
# 重要的模型部件
|
||||
important_components = [
|
||||
"body.obj",
|
||||
"trigger.obj",
|
||||
"trackpad.obj",
|
||||
"l_grip.obj" if self.controller.name == 'left' else "r_grip.obj"
|
||||
]
|
||||
|
||||
combined_model = NodePath("combined_controller")
|
||||
has_components = False
|
||||
|
||||
for component in important_components:
|
||||
component_path = os.path.join(base_path, component)
|
||||
if os.path.exists(component_path):
|
||||
try:
|
||||
part = loader.loadModel(component_path)
|
||||
if part:
|
||||
part.reparentTo(combined_model)
|
||||
has_components = True
|
||||
print(f"✅ 加载了部件: {component}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 加载部件失败 {component}: {e}")
|
||||
|
||||
if has_components:
|
||||
self._apply_steamvr_textures(combined_model, base_path)
|
||||
return combined_model
|
||||
|
||||
return None
|
||||
|
||||
def _create_fallback_model(self):
|
||||
"""创建改进的程序化手柄模型作为后备方案"""
|
||||
# 主体(长条形状)
|
||||
main_body = self._create_box_geometry(0.025, 0.15, 0.04)
|
||||
main_node = self.model_node.attachNewNode(main_body)
|
||||
|
||||
# 根据左右手设置不同颜色
|
||||
if self.controller.name == 'left':
|
||||
color = self.button_colors['left']
|
||||
else:
|
||||
color = self.button_colors['right']
|
||||
|
||||
main_node.setColor(color)
|
||||
|
||||
# 启用光照响应
|
||||
from panda3d.core import RenderState, MaterialAttrib, Material
|
||||
material = Material()
|
||||
material.setShininess(32)
|
||||
material.setAmbient((0.2, 0.2, 0.2, 1))
|
||||
material.setDiffuse(color)
|
||||
material.setSpecular((0.5, 0.5, 0.5, 1))
|
||||
main_node.setMaterial(material)
|
||||
|
||||
# 扳机区域(小突起)
|
||||
trigger = self._create_box_geometry(0.015, 0.03, 0.02)
|
||||
trigger_node = self.model_node.attachNewNode(trigger)
|
||||
trigger_node.setPos(0, -0.08, 0.03)
|
||||
trigger_node.setColor(self.button_colors['trigger'])
|
||||
trigger_node.setMaterial(material)
|
||||
|
||||
# 握把区域
|
||||
grip = self._create_box_geometry(0.02, 0.06, 0.03)
|
||||
grip_node = self.model_node.attachNewNode(grip)
|
||||
grip_node.setPos(0, 0.05, -0.03)
|
||||
grip_node.setColor(self.button_colors['grip'])
|
||||
grip_node.setMaterial(material)
|
||||
|
||||
# 触摸板区域(圆盘)
|
||||
trackpad = self._create_disc_geometry(0.015, 0.005)
|
||||
trackpad_node = self.model_node.attachNewNode(trackpad)
|
||||
trackpad_node.setPos(0, -0.02, 0.04)
|
||||
trackpad_node.setColor(self.button_colors['trackpad'])
|
||||
trackpad_node.setMaterial(material)
|
||||
|
||||
def _create_box_geometry(self, width, length, height):
|
||||
"""创建立方体几何体"""
|
||||
# 创建顶点格式
|
||||
format = GeomVertexFormat.getV3n3()
|
||||
vdata = GeomVertexData('box', format, Geom.UHStatic)
|
||||
|
||||
vertex = GeomVertexWriter(vdata, 'vertex')
|
||||
normal = GeomVertexWriter(vdata, 'normal')
|
||||
|
||||
# 定义立方体的8个顶点
|
||||
vertices = [
|
||||
Vec3(-width/2, -length/2, -height/2),
|
||||
Vec3( width/2, -length/2, -height/2),
|
||||
Vec3( width/2, length/2, -height/2),
|
||||
Vec3(-width/2, length/2, -height/2),
|
||||
Vec3(-width/2, -length/2, height/2),
|
||||
Vec3( width/2, -length/2, height/2),
|
||||
Vec3( width/2, length/2, height/2),
|
||||
Vec3(-width/2, length/2, height/2)
|
||||
]
|
||||
|
||||
# 立方体的6个面,每个面4个顶点
|
||||
faces = [
|
||||
# 底面 (z = -height/2)
|
||||
[0, 1, 2, 3, Vec3(0, 0, -1)],
|
||||
# 顶面 (z = height/2)
|
||||
[7, 6, 5, 4, Vec3(0, 0, 1)],
|
||||
# 前面 (y = -length/2)
|
||||
[4, 5, 1, 0, Vec3(0, -1, 0)],
|
||||
# 后面 (y = length/2)
|
||||
[3, 2, 6, 7, Vec3(0, 1, 0)],
|
||||
# 左面 (x = -width/2)
|
||||
[0, 3, 7, 4, Vec3(-1, 0, 0)],
|
||||
# 右面 (x = width/2)
|
||||
[5, 6, 2, 1, Vec3(1, 0, 0)]
|
||||
]
|
||||
|
||||
# 添加顶点和法线
|
||||
for face in faces:
|
||||
for i in range(4):
|
||||
vertex.addData3(vertices[face[i]])
|
||||
normal.addData3(face[4]) # 法线向量
|
||||
|
||||
# 创建几何体
|
||||
geom = Geom(vdata)
|
||||
|
||||
# 为每个面创建三角形
|
||||
for face_idx in range(6):
|
||||
base_idx = face_idx * 4
|
||||
prim = GeomTriangles(Geom.UHStatic)
|
||||
# 第一个三角形
|
||||
prim.addVertices(base_idx, base_idx + 1, base_idx + 2)
|
||||
# 第二个三角形
|
||||
prim.addVertices(base_idx, base_idx + 2, base_idx + 3)
|
||||
geom.addPrimitive(prim)
|
||||
|
||||
# 创建几何体节点
|
||||
geom_node = GeomNode('box')
|
||||
geom_node.addGeom(geom)
|
||||
|
||||
return geom_node
|
||||
|
||||
def _create_disc_geometry(self, radius, thickness):
|
||||
"""创建圆盘几何体(用于触摸板)"""
|
||||
format = GeomVertexFormat.getV3n3()
|
||||
vdata = GeomVertexData('disc', format, Geom.UHStatic)
|
||||
|
||||
vertex = GeomVertexWriter(vdata, 'vertex')
|
||||
normal = GeomVertexWriter(vdata, 'normal')
|
||||
|
||||
# 创建圆盘顶点
|
||||
segments = 16
|
||||
import math
|
||||
|
||||
# 中心点
|
||||
vertex.addData3(0, 0, thickness/2)
|
||||
normal.addData3(0, 0, 1)
|
||||
|
||||
# 圆周点
|
||||
for i in range(segments):
|
||||
angle = 2 * math.pi * i / segments
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
|
||||
vertex.addData3(x, y, thickness/2)
|
||||
normal.addData3(0, 0, 1)
|
||||
|
||||
# 创建几何体
|
||||
geom = Geom(vdata)
|
||||
prim = GeomTriangles(Geom.UHStatic)
|
||||
|
||||
# 创建扇形三角形
|
||||
for i in range(segments):
|
||||
next_i = (i + 1) % segments
|
||||
prim.addVertices(0, i + 1, next_i + 1)
|
||||
|
||||
geom.addPrimitive(prim)
|
||||
|
||||
# 创建几何体节点
|
||||
geom_node = GeomNode('disc')
|
||||
geom_node.addGeom(geom)
|
||||
|
||||
return geom_node
|
||||
|
||||
def _create_interaction_ray(self):
|
||||
"""创建交互射线"""
|
||||
if not self.visual_node:
|
||||
return
|
||||
|
||||
# 创建射线几何
|
||||
line_segs = LineSegs()
|
||||
line_segs.setThickness(3)
|
||||
line_segs.setColor(self.ray_color)
|
||||
|
||||
# 射线主体
|
||||
line_segs.moveTo(0, 0, 0)
|
||||
line_segs.drawTo(0, self.ray_length, 0)
|
||||
|
||||
# 射线端点(小球)
|
||||
end_point = self._create_sphere_geometry(0.02)
|
||||
|
||||
# 创建射线节点
|
||||
geom_node = line_segs.create()
|
||||
self.ray_node = self.visual_node.attachNewNode(geom_node)
|
||||
|
||||
# 添加端点球
|
||||
end_node = self.ray_node.attachNewNode(end_point)
|
||||
end_node.setPos(0, self.ray_length, 0)
|
||||
end_node.setColor(self.ray_color)
|
||||
|
||||
# 设置透明度
|
||||
self.ray_node.setTransparency(TransparencyAttrib.MAlpha)
|
||||
|
||||
# 默认隐藏射线
|
||||
self.ray_node.hide()
|
||||
|
||||
print(f"✓ {self.controller.name}手柄交互射线已创建")
|
||||
|
||||
def _create_sphere_geometry(self, radius):
|
||||
"""创建球体几何体"""
|
||||
# 简单的立方体作为球体替代
|
||||
return self._create_box_geometry(radius, radius, radius)
|
||||
|
||||
def _create_button_indicators(self):
|
||||
"""创建按钮状态指示器"""
|
||||
if not self.visual_node:
|
||||
return
|
||||
|
||||
# 创建按钮指示器容器
|
||||
self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators')
|
||||
|
||||
# 扳机指示器
|
||||
trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005)
|
||||
self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator)
|
||||
self.trigger_indicator.setPos(0.02, -0.08, 0.03)
|
||||
self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
# 握把指示器
|
||||
grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005)
|
||||
self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator)
|
||||
self.grip_indicator.setPos(-0.02, 0.05, -0.03)
|
||||
self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
# 触摸板指示器
|
||||
trackpad_indicator = self._create_disc_geometry(0.003, 0.002)
|
||||
self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator)
|
||||
self.trackpad_indicator.setPos(0, -0.02, 0.045)
|
||||
self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
print(f"✓ {self.controller.name}手柄按钮指示器已创建")
|
||||
|
||||
def update(self):
|
||||
"""更新可视化状态"""
|
||||
if not self.controller.is_connected:
|
||||
self.hide()
|
||||
return
|
||||
|
||||
self.show()
|
||||
|
||||
# 更新按钮指示器状态
|
||||
self._update_button_indicators()
|
||||
|
||||
# 更新射线显示状态
|
||||
self._update_ray_display()
|
||||
|
||||
def _update_button_indicators(self):
|
||||
"""更新按钮指示器状态"""
|
||||
if not hasattr(self, 'trigger_indicator'):
|
||||
return
|
||||
|
||||
# 扳机指示器
|
||||
if self.controller.is_trigger_pressed():
|
||||
self.trigger_indicator.setColor(self.button_colors['trigger'])
|
||||
# 根据扳机值调整位置
|
||||
trigger_offset = self.controller.trigger_value * 0.01
|
||||
self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03)
|
||||
else:
|
||||
self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
self.trigger_indicator.setPos(0.02, -0.08, 0.03)
|
||||
|
||||
# 握把指示器
|
||||
if self.controller.is_grip_pressed():
|
||||
self.grip_indicator.setColor(self.button_colors['grip'])
|
||||
else:
|
||||
self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
# 触摸板指示器
|
||||
if self.controller.touchpad_touched:
|
||||
self.trackpad_indicator.setColor(self.button_colors['trackpad'])
|
||||
# 根据触摸位置调整指示器位置
|
||||
if hasattr(self.controller, 'touchpad_pos'):
|
||||
offset_x = self.controller.touchpad_pos.x * 0.01
|
||||
offset_y = self.controller.touchpad_pos.y * 0.01
|
||||
self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045)
|
||||
else:
|
||||
self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0)
|
||||
self.trackpad_indicator.setPos(0, -0.02, 0.045)
|
||||
|
||||
def _update_ray_display(self):
|
||||
"""更新射线显示"""
|
||||
if not self.ray_node:
|
||||
return
|
||||
|
||||
# 根据交互状态显示/隐藏射线
|
||||
# 这里可以添加更复杂的逻辑,比如只在指向对象时显示
|
||||
show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or
|
||||
self.controller.touchpad_touched)
|
||||
|
||||
if show_ray:
|
||||
self.show_ray()
|
||||
else:
|
||||
self.hide_ray()
|
||||
|
||||
def show(self):
|
||||
"""显示手柄可视化"""
|
||||
if self.visual_node:
|
||||
self.visual_node.show()
|
||||
|
||||
def hide(self):
|
||||
"""隐藏手柄可视化"""
|
||||
if self.visual_node:
|
||||
self.visual_node.hide()
|
||||
|
||||
def show_ray(self):
|
||||
"""显示交互射线"""
|
||||
if self.ray_node:
|
||||
self.ray_node.show()
|
||||
|
||||
def hide_ray(self):
|
||||
"""隐藏交互射线"""
|
||||
if self.ray_node:
|
||||
self.ray_node.hide()
|
||||
|
||||
def set_ray_color(self, color):
|
||||
"""设置射线颜色"""
|
||||
if self.ray_node:
|
||||
self.ray_node.setColor(color)
|
||||
|
||||
def set_ray_length(self, length):
|
||||
"""设置射线长度"""
|
||||
self.ray_length = length
|
||||
# 重新创建射线(简单的实现)
|
||||
if self.ray_node:
|
||||
self.ray_node.removeNode()
|
||||
self._create_interaction_ray()
|
||||
|
||||
def _set_always_on_top(self, model_node):
|
||||
"""设置手柄模型始终显示在上层,不被其他物体遮挡"""
|
||||
if not model_node:
|
||||
return
|
||||
|
||||
from panda3d.core import RenderState
|
||||
|
||||
# 设置为固定渲染bin,优先级设为较高值(1000)
|
||||
# fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层)
|
||||
model_node.setBin("fixed", 1000)
|
||||
|
||||
# 禁用深度测试和深度写入,确保始终可见
|
||||
model_node.setDepthTest(False)
|
||||
model_node.setDepthWrite(False)
|
||||
|
||||
# 递归设置所有子节点的渲染属性
|
||||
for child in model_node.findAllMatches("**"):
|
||||
child.setBin("fixed", 1000)
|
||||
child.setDepthTest(False)
|
||||
child.setDepthWrite(False)
|
||||
|
||||
print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层")
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
if self.visual_node:
|
||||
self.visual_node.removeNode()
|
||||
|
||||
print(f"🧹 {self.controller.name}手柄可视化已清理")
|
||||
89
vr_actions/actions.json
Normal file
89
vr_actions/actions.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"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": "震动反馈"
|
||||
}
|
||||
]
|
||||
}
|
||||
106
vr_actions/bindings_vive.json
Normal file
106
vr_actions/bindings_vive.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"controller_type": "vive_controller",
|
||||
"description": "Vive\u63a7\u5236\u5668\u7ed1\u5b9a",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user