VR #3
112
CLAUDE.md
Normal file
112
CLAUDE.md
Normal file
@ -0,0 +1,112 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于Panda3D的3D渲染引擎和场景编辑器,集成了PyQt5界面和多种高级功能:
|
||||
|
||||
- 3D场景编辑器(模型导入、材质系统、碰撞检测)
|
||||
- GUI元素管理(2D/3D GUI组件)
|
||||
- 项目管理系统(场景保存/加载、项目打包)
|
||||
- Cesium地图集成
|
||||
- 渲染管线增强(RenderPipelineFile)
|
||||
|
||||
## 运行和构建命令
|
||||
|
||||
### 启动应用程序
|
||||
```bash
|
||||
python Start_Run.py [project_path]
|
||||
```
|
||||
或者直接:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 依赖安装
|
||||
```bash
|
||||
# 主要依赖
|
||||
pip install -r requirements/requirements.txt
|
||||
|
||||
# Conda环境依赖
|
||||
pip install -r requirements/conda-requirements.txt
|
||||
```
|
||||
|
||||
### 工具脚本
|
||||
```bash
|
||||
# 安装FBX到GLTF转换工具
|
||||
./install_fbx2gltf.sh
|
||||
```
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 主要模块结构
|
||||
```
|
||||
EG/
|
||||
├── main.py # 应用程序入口点
|
||||
├── Start_Run.py # 启动脚本(路径配置)
|
||||
├── core/ # 核心功能模块
|
||||
│ ├── world.py # 3D世界核心(继承Panda3DWorld)
|
||||
│ ├── scene_manager.py # 场景和模型管理
|
||||
│ ├── selection.py # 对象选择系统
|
||||
│ ├── event_handler.py # 事件处理
|
||||
│ └── tool_manager.py # 工具系统
|
||||
├── gui/ # GUI元素管理
|
||||
│ └── gui_manager.py # 2D/3D GUI组件
|
||||
├── ui/ # 用户界面
|
||||
│ ├── widgets.py # 自定义Qt组件
|
||||
│ ├── property_panel.py # 属性面板
|
||||
│ └── interface_manager.py # 界面管理
|
||||
├── scene/ # 场景相关
|
||||
│ └── scene_manager.py # 场景管理器
|
||||
├── project/ # 项目管理
|
||||
│ └── project_manager.py # 项目生命周期
|
||||
├── RenderPipelineFile/ # 渲染管线扩展
|
||||
└── QPanda3D/ # Panda3D Qt集成
|
||||
```
|
||||
|
||||
### 核心设计模式
|
||||
|
||||
1. **模块化架构**: 每个功能模块独立,通过管理器类协调
|
||||
2. **事件驱动**: EventHandler统一处理用户交互和系统事件
|
||||
3. **组件系统**: SelectionSystem、ToolManager等可插拔组件
|
||||
4. **MVC分离**: UI组件、核心逻辑和数据管理分离
|
||||
|
||||
### 主要依赖集成
|
||||
|
||||
- **Panda3D 1.10.15**: 3D渲染引擎
|
||||
- **PyQt5**: GUI框架
|
||||
- **QPanda3D**: Panda3D的Qt集成
|
||||
- **RenderPipeline**: 高级渲染功能
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新功能模块
|
||||
1. 在对应目录下创建新的Python文件
|
||||
2. 继承相应的基类(如Panda3DWorld用于3D功能)
|
||||
3. 在main.py中集成新模块
|
||||
4. 更新界面管理器以添加UI控制
|
||||
|
||||
### 材质和渲染
|
||||
- 材质系统集成在scene/scene_manager.py
|
||||
- 支持PBR材质和自定义着色器
|
||||
- RenderPipelineFile提供高级渲染特性
|
||||
|
||||
|
||||
### GUI开发
|
||||
- 使用PyQt5构建主界面
|
||||
- 3D GUI元素通过gui/gui_manager.py管理
|
||||
- 自定义组件在ui/widgets.py中定义
|
||||
|
||||
## 文件约定
|
||||
|
||||
- Python文件使用UTF-8编码
|
||||
- 中文注释和文档字符串
|
||||
- 模块顶部包含功能描述注释
|
||||
- 类和方法使用描述性命名
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 项目依赖多个大型库(Panda3D、PyQt5、RenderPipeline)
|
||||
- Cesium集成需要WebEngine支持
|
||||
- 某些功能可能需要特定的系统配置
|
||||
@ -53,7 +53,7 @@ class Application(ShowBase):
|
||||
# ------ End of render pipeline code, thats it! ------
|
||||
|
||||
# Set time of day
|
||||
self.render_pipeline.daytime_mgr.time = "12:43"
|
||||
self.render_pipeline.daytime_mgr.time = "6:43"
|
||||
|
||||
# Load the scene
|
||||
model = loader.loadModel("scene/Scene.bam")
|
||||
|
||||
@ -52,11 +52,6 @@ class MainApp(ShowBase):
|
||||
# Load the scene
|
||||
model = loader.loadModel("scene/scene.bam")
|
||||
# model = loader.loadModel("scene2/Scene.bam")
|
||||
model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx")
|
||||
model_0.reparentTo(self.render)
|
||||
model_0.setScale(0.01)
|
||||
model_0.setPos(-8, 42, 0)
|
||||
model_0.setHpr(0, 90, 0)
|
||||
|
||||
model.reparent_to(render)
|
||||
self.render_pipeline.prepare_scene(model)
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
# Autogenerated
|
||||
name = 'Plastic-R0.0'
|
||||
roughness = 0.0
|
||||
ior = 1.51
|
||||
basecolor = (1, 0, 0)
|
||||
mat_type = 'default'
|
||||
@ -1,509 +0,0 @@
|
||||
"""
|
||||
ALVR串流处理器
|
||||
|
||||
负责与ALVR服务器通信和视频流传输
|
||||
支持Quest等VR头显的无线串流
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
import psutil
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
from panda3d.core import Texture, PNMImage
|
||||
|
||||
|
||||
class ALVRStreamer(DirectObject):
|
||||
"""ALVR串流处理器"""
|
||||
|
||||
def __init__(self, world, vr_manager):
|
||||
super().__init__()
|
||||
self.world = world
|
||||
self.vr_manager = vr_manager
|
||||
|
||||
# ALVR服务器配置
|
||||
self.alvr_server_ip = "127.0.0.1"
|
||||
self.alvr_server_port = 9943
|
||||
self.alvr_streaming_port = 9944
|
||||
|
||||
# 连接状态
|
||||
self.connected = False
|
||||
self.streaming = False
|
||||
self.server_socket = None
|
||||
self.streaming_socket = None
|
||||
|
||||
# 流媒体配置
|
||||
self.stream_width = 2880 # Quest 2 推荐分辨率
|
||||
self.stream_height = 1700
|
||||
self.stream_fps = 72
|
||||
self.bitrate = 150 # Mbps
|
||||
self.codec = "h264"
|
||||
|
||||
# 线程管理
|
||||
self.connection_thread = None
|
||||
self.streaming_thread = None
|
||||
self.running = False
|
||||
|
||||
# 性能统计
|
||||
self.frame_count = 0
|
||||
self.last_fps_time = time.time()
|
||||
self.current_fps = 0
|
||||
self.latency = 0
|
||||
|
||||
print("✓ ALVR串流处理器初始化完成")
|
||||
|
||||
def initialize(self):
|
||||
"""初始化ALVR串流"""
|
||||
try:
|
||||
# 检查ALVR服务器是否运行
|
||||
if not self._check_alvr_server():
|
||||
print("ALVR服务器未运行,尝试启动...")
|
||||
if not self._start_alvr_server():
|
||||
print("无法启动ALVR服务器")
|
||||
return False
|
||||
|
||||
# 连接到ALVR服务器
|
||||
if not self._connect_to_server():
|
||||
print("无法连接到ALVR服务器")
|
||||
return False
|
||||
|
||||
# 配置流媒体设置
|
||||
self._configure_streaming()
|
||||
|
||||
# 启动串流线程
|
||||
self._start_streaming_threads()
|
||||
|
||||
print("✓ ALVR串流初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"ALVR初始化错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def _check_alvr_server(self):
|
||||
"""检查ALVR服务器是否运行"""
|
||||
try:
|
||||
# 检查进程
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
if 'alvr' in proc.info['name'].lower():
|
||||
return True
|
||||
|
||||
# 尝试连接端口
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(2)
|
||||
result = sock.connect_ex((self.alvr_server_ip, self.alvr_server_port))
|
||||
sock.close()
|
||||
|
||||
return result == 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"检查ALVR服务器错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def _start_alvr_server(self):
|
||||
"""启动ALVR服务器"""
|
||||
try:
|
||||
# 尝试启动ALVR服务器
|
||||
# 这里需要根据实际的ALVR安装路径调整
|
||||
alvr_paths = [
|
||||
"/usr/local/bin/alvr_server",
|
||||
"/usr/bin/alvr_server",
|
||||
"C:/Program Files/ALVR/alvr_server.exe",
|
||||
"C:/ALVR/alvr_server.exe"
|
||||
]
|
||||
|
||||
for path in alvr_paths:
|
||||
try:
|
||||
subprocess.Popen([path], shell=True)
|
||||
time.sleep(3) # 等待服务器启动
|
||||
if self._check_alvr_server():
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"启动ALVR服务器错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def _connect_to_server(self):
|
||||
"""连接到ALVR服务器"""
|
||||
try:
|
||||
# 创建TCP连接
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.settimeout(5)
|
||||
self.server_socket.connect((self.alvr_server_ip, self.alvr_server_port))
|
||||
|
||||
# 发送握手消息
|
||||
handshake_data = {
|
||||
"type": "handshake",
|
||||
"client_name": "Panda3D_VR_Engine",
|
||||
"version": "1.0",
|
||||
"capabilities": {
|
||||
"video": True,
|
||||
"audio": True,
|
||||
"tracking": True,
|
||||
"haptics": True
|
||||
}
|
||||
}
|
||||
|
||||
self._send_message(handshake_data)
|
||||
|
||||
# 接收响应
|
||||
response = self._receive_message()
|
||||
if response and response.get("type") == "handshake_response":
|
||||
self.connected = True
|
||||
print("✓ 已连接到ALVR服务器")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"连接ALVR服务器错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def _send_message(self, data):
|
||||
"""发送消息到ALVR服务器"""
|
||||
try:
|
||||
message = json.dumps(data).encode('utf-8')
|
||||
length = struct.pack('<I', len(message))
|
||||
self.server_socket.send(length + message)
|
||||
except Exception as e:
|
||||
print(f"发送消息错误: {str(e)}")
|
||||
|
||||
def _receive_message(self):
|
||||
"""从ALVR服务器接收消息"""
|
||||
try:
|
||||
# 接收消息长度
|
||||
length_data = self.server_socket.recv(4)
|
||||
if not length_data:
|
||||
return None
|
||||
|
||||
length = struct.unpack('<I', length_data)[0]
|
||||
|
||||
# 接收消息内容
|
||||
message_data = b''
|
||||
while len(message_data) < length:
|
||||
chunk = self.server_socket.recv(length - len(message_data))
|
||||
if not chunk:
|
||||
return None
|
||||
message_data += chunk
|
||||
|
||||
return json.loads(message_data.decode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"接收消息错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def _configure_streaming(self):
|
||||
"""配置流媒体设置"""
|
||||
try:
|
||||
# 发送流媒体配置
|
||||
config_data = {
|
||||
"type": "stream_config",
|
||||
"video": {
|
||||
"width": self.stream_width,
|
||||
"height": self.stream_height,
|
||||
"fps": self.stream_fps,
|
||||
"bitrate": self.bitrate,
|
||||
"codec": self.codec
|
||||
},
|
||||
"audio": {
|
||||
"enabled": True,
|
||||
"sample_rate": 48000,
|
||||
"channels": 2
|
||||
}
|
||||
}
|
||||
|
||||
self._send_message(config_data)
|
||||
|
||||
# 接收配置响应
|
||||
response = self._receive_message()
|
||||
if response and response.get("type") == "config_response":
|
||||
if response.get("status") == "success":
|
||||
print("✓ 流媒体配置成功")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"配置流媒体错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def _start_streaming_threads(self):
|
||||
"""启动串流线程"""
|
||||
self.running = True
|
||||
|
||||
# 启动连接管理线程
|
||||
self.connection_thread = threading.Thread(target=self._connection_handler)
|
||||
self.connection_thread.daemon = True
|
||||
self.connection_thread.start()
|
||||
|
||||
# 启动流媒体线程
|
||||
self.streaming_thread = threading.Thread(target=self._streaming_handler)
|
||||
self.streaming_thread.daemon = True
|
||||
self.streaming_thread.start()
|
||||
|
||||
def _connection_handler(self):
|
||||
"""连接处理线程"""
|
||||
while self.running:
|
||||
try:
|
||||
if self.connected:
|
||||
# 发送心跳
|
||||
heartbeat = {"type": "heartbeat", "timestamp": time.time()}
|
||||
self._send_message(heartbeat)
|
||||
|
||||
# 接收消息
|
||||
response = self._receive_message()
|
||||
if response:
|
||||
self._handle_server_message(response)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"连接处理错误: {str(e)}")
|
||||
self.connected = False
|
||||
time.sleep(1)
|
||||
|
||||
def _streaming_handler(self):
|
||||
"""流媒体处理线程"""
|
||||
while self.running:
|
||||
try:
|
||||
if self.connected and self.streaming:
|
||||
# 获取VR渲染帧
|
||||
frame_data = self._get_vr_frame()
|
||||
if frame_data:
|
||||
# 发送帧数据
|
||||
self._send_frame(frame_data)
|
||||
|
||||
# 更新性能统计
|
||||
self._update_performance_stats()
|
||||
|
||||
time.sleep(1.0 / self.stream_fps)
|
||||
|
||||
except Exception as e:
|
||||
print(f"流媒体处理错误: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
def _handle_server_message(self, message):
|
||||
"""处理服务器消息"""
|
||||
msg_type = message.get("type")
|
||||
|
||||
if msg_type == "start_streaming":
|
||||
self.streaming = True
|
||||
print("✓ 开始VR串流")
|
||||
|
||||
elif msg_type == "stop_streaming":
|
||||
self.streaming = False
|
||||
print("✓ 停止VR串流")
|
||||
|
||||
elif msg_type == "client_connected":
|
||||
print(f"✓ VR客户端已连接: {message.get('client_info', {})}")
|
||||
|
||||
elif msg_type == "client_disconnected":
|
||||
print("✓ VR客户端已断开")
|
||||
|
||||
elif msg_type == "tracking_data":
|
||||
self._handle_tracking_data(message.get("data"))
|
||||
|
||||
elif msg_type == "haptic_feedback":
|
||||
self._handle_haptic_feedback(message.get("data"))
|
||||
|
||||
def _handle_tracking_data(self, tracking_data):
|
||||
"""处理跟踪数据"""
|
||||
if not tracking_data:
|
||||
return
|
||||
|
||||
# 更新VR管理器的跟踪数据
|
||||
# 这里可以处理从ALVR客户端发送的跟踪数据
|
||||
pass
|
||||
|
||||
def _handle_haptic_feedback(self, haptic_data):
|
||||
"""处理触觉反馈"""
|
||||
if not haptic_data:
|
||||
return
|
||||
|
||||
# 处理触觉反馈请求
|
||||
# 这里可以控制VR控制器的震动等
|
||||
pass
|
||||
|
||||
def _get_vr_frame(self):
|
||||
"""获取VR渲染帧"""
|
||||
try:
|
||||
if not self.vr_manager.is_vr_enabled():
|
||||
return None
|
||||
|
||||
# 获取左右眼纹理
|
||||
left_texture = self.vr_manager.eye_textures.get('left')
|
||||
right_texture = self.vr_manager.eye_textures.get('right')
|
||||
|
||||
if not left_texture or not right_texture:
|
||||
return None
|
||||
|
||||
# 合成立体帧
|
||||
frame_data = self._compose_stereo_frame(left_texture, right_texture)
|
||||
return frame_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取VR帧错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def _compose_stereo_frame(self, left_texture, right_texture):
|
||||
"""合成立体帧"""
|
||||
try:
|
||||
# 创建组合图像
|
||||
combined_image = PNMImage(self.stream_width, self.stream_height)
|
||||
|
||||
# 获取左右眼图像
|
||||
left_image = PNMImage()
|
||||
right_image = PNMImage()
|
||||
|
||||
if left_texture.store(left_image) and right_texture.store(right_image):
|
||||
# 将左右眼图像合并(Side-by-Side布局)
|
||||
left_width = self.stream_width // 2
|
||||
|
||||
# 缩放左眼图像到左半部分
|
||||
left_scaled = PNMImage(left_width, self.stream_height)
|
||||
left_scaled.quickFilterFrom(left_image)
|
||||
combined_image.copySubImage(left_scaled, 0, 0)
|
||||
|
||||
# 缩放右眼图像到右半部分
|
||||
right_scaled = PNMImage(left_width, self.stream_height)
|
||||
right_scaled.quickFilterFrom(right_image)
|
||||
combined_image.copySubImage(right_scaled, left_width, 0)
|
||||
|
||||
# 转换为字节数据
|
||||
return combined_image.makeRamImage()
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"合成立体帧错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def _send_frame(self, frame_data):
|
||||
"""发送帧数据"""
|
||||
try:
|
||||
if not self.server_socket:
|
||||
return
|
||||
|
||||
# 创建帧消息
|
||||
frame_message = {
|
||||
"type": "video_frame",
|
||||
"timestamp": time.time(),
|
||||
"width": self.stream_width,
|
||||
"height": self.stream_height,
|
||||
"format": "rgb",
|
||||
"data": frame_data.hex() # 转换为十六进制字符串
|
||||
}
|
||||
|
||||
self._send_message(frame_message)
|
||||
|
||||
except Exception as e:
|
||||
print(f"发送帧错误: {str(e)}")
|
||||
|
||||
def _update_performance_stats(self):
|
||||
"""更新性能统计"""
|
||||
self.frame_count += 1
|
||||
current_time = time.time()
|
||||
|
||||
if current_time - self.last_fps_time >= 1.0:
|
||||
self.current_fps = self.frame_count
|
||||
self.frame_count = 0
|
||||
self.last_fps_time = current_time
|
||||
|
||||
def start_streaming(self):
|
||||
"""开始串流"""
|
||||
if not self.connected:
|
||||
print("未连接到ALVR服务器")
|
||||
return False
|
||||
|
||||
start_message = {"type": "start_streaming"}
|
||||
self._send_message(start_message)
|
||||
return True
|
||||
|
||||
def stop_streaming(self):
|
||||
"""停止串流"""
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
stop_message = {"type": "stop_streaming"}
|
||||
self._send_message(stop_message)
|
||||
self.streaming = False
|
||||
|
||||
def send_haptic_feedback(self, controller_id, duration, intensity):
|
||||
"""发送触觉反馈"""
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
haptic_message = {
|
||||
"type": "haptic_feedback",
|
||||
"controller_id": controller_id,
|
||||
"duration": duration,
|
||||
"intensity": intensity
|
||||
}
|
||||
|
||||
self._send_message(haptic_message)
|
||||
|
||||
def get_streaming_status(self):
|
||||
"""获取串流状态"""
|
||||
return {
|
||||
"connected": self.connected,
|
||||
"streaming": self.streaming,
|
||||
"fps": self.current_fps,
|
||||
"latency": self.latency,
|
||||
"resolution": f"{self.stream_width}x{self.stream_height}",
|
||||
"bitrate": self.bitrate
|
||||
}
|
||||
|
||||
def set_stream_quality(self, width, height, fps, bitrate):
|
||||
"""设置串流质量"""
|
||||
self.stream_width = width
|
||||
self.stream_height = height
|
||||
self.stream_fps = fps
|
||||
self.bitrate = bitrate
|
||||
|
||||
# 如果正在串流,重新配置
|
||||
if self.streaming:
|
||||
self._configure_streaming()
|
||||
|
||||
def shutdown(self):
|
||||
"""关闭串流"""
|
||||
print("关闭ALVR串流...")
|
||||
|
||||
self.running = False
|
||||
self.streaming = False
|
||||
|
||||
# 发送断开消息
|
||||
if self.connected:
|
||||
disconnect_message = {"type": "disconnect"}
|
||||
self._send_message(disconnect_message)
|
||||
|
||||
# 关闭连接
|
||||
if self.server_socket:
|
||||
self.server_socket.close()
|
||||
|
||||
if self.streaming_socket:
|
||||
self.streaming_socket.close()
|
||||
|
||||
# 等待线程结束
|
||||
if self.connection_thread:
|
||||
self.connection_thread.join(timeout=2)
|
||||
|
||||
if self.streaming_thread:
|
||||
self.streaming_thread.join(timeout=2)
|
||||
|
||||
self.connected = False
|
||||
print("✓ ALVR串流已关闭")
|
||||
|
||||
def is_connected(self):
|
||||
"""检查是否连接"""
|
||||
return self.connected
|
||||
|
||||
def is_streaming(self):
|
||||
"""检查是否在串流"""
|
||||
return self.streaming
|
||||
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')
|
||||
@ -1,430 +0,0 @@
|
||||
"""
|
||||
VR输入处理器
|
||||
|
||||
处理VR控制器输入、手势识别和VR交互
|
||||
支持多种VR控制器和手势输入
|
||||
"""
|
||||
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
from panda3d.core import Vec3, Point3, CollisionRay, CollisionNode, CollisionHandlerQueue
|
||||
from direct.task import Task
|
||||
import time
|
||||
|
||||
|
||||
class VRInputHandler(DirectObject):
|
||||
"""VR输入处理器"""
|
||||
|
||||
def __init__(self, world, vr_manager):
|
||||
super().__init__()
|
||||
self.world = world
|
||||
self.vr_manager = vr_manager
|
||||
|
||||
# 控制器状态
|
||||
self.controllers = {}
|
||||
self.controller_nodes = {}
|
||||
self.controller_rays = {}
|
||||
|
||||
# 手势识别
|
||||
self.gesture_enabled = True
|
||||
self.gesture_history = []
|
||||
self.gesture_threshold = 0.1
|
||||
|
||||
# 交互系统
|
||||
self.interaction_enabled = True
|
||||
self.selected_object = None
|
||||
self.grab_offset = Vec3(0, 0, 0)
|
||||
|
||||
# 输入映射
|
||||
self.input_mappings = {
|
||||
'trigger': self._handle_trigger,
|
||||
'grip': self._handle_grip,
|
||||
'touchpad': self._handle_touchpad,
|
||||
'menu': self._handle_menu,
|
||||
'system': self._handle_system
|
||||
}
|
||||
|
||||
print("✓ VR输入处理器初始化完成")
|
||||
|
||||
def start_input_handling(self):
|
||||
"""启动输入处理"""
|
||||
if not self.vr_manager.is_vr_enabled():
|
||||
print("VR未启用,无法启动输入处理")
|
||||
return False
|
||||
|
||||
# 启动输入更新任务
|
||||
self.world.taskMgr.add(self._update_input, "vr_input_update")
|
||||
|
||||
# 设置控制器可视化
|
||||
self._setup_controller_visualization()
|
||||
|
||||
print("✓ VR输入处理已启动")
|
||||
return True
|
||||
|
||||
def stop_input_handling(self):
|
||||
"""停止输入处理"""
|
||||
self.world.taskMgr.remove("vr_input_update")
|
||||
self._cleanup_controller_visualization()
|
||||
print("✓ VR输入处理已停止")
|
||||
|
||||
def _update_input(self, task):
|
||||
"""更新输入处理"""
|
||||
if not self.vr_manager.is_vr_enabled():
|
||||
return Task.cont
|
||||
|
||||
try:
|
||||
# 更新所有控制器
|
||||
self._update_controllers()
|
||||
|
||||
# 处理手势识别
|
||||
if self.gesture_enabled:
|
||||
self._process_gestures()
|
||||
|
||||
# 处理交互
|
||||
if self.interaction_enabled:
|
||||
self._process_interactions()
|
||||
|
||||
except Exception as e:
|
||||
print(f"VR输入更新错误: {str(e)}")
|
||||
|
||||
return Task.cont
|
||||
|
||||
def _update_controllers(self):
|
||||
"""更新控制器状态"""
|
||||
# 获取控制器姿态
|
||||
controller_poses = self.vr_manager.controller_poses
|
||||
|
||||
for controller_id, pose in controller_poses.items():
|
||||
# 获取控制器输入
|
||||
input_data = self.vr_manager.get_controller_input(controller_id)
|
||||
if not input_data:
|
||||
continue
|
||||
|
||||
# 更新控制器状态
|
||||
if controller_id not in self.controllers:
|
||||
self.controllers[controller_id] = {}
|
||||
|
||||
prev_state = self.controllers[controller_id].copy()
|
||||
self.controllers[controller_id] = input_data
|
||||
|
||||
# 更新控制器可视化
|
||||
self._update_controller_visualization(controller_id, pose)
|
||||
|
||||
# 处理输入事件
|
||||
self._process_controller_input(controller_id, input_data, prev_state)
|
||||
|
||||
def _process_controller_input(self, controller_id, current_state, prev_state):
|
||||
"""处理控制器输入"""
|
||||
# 检查按钮状态变化
|
||||
for input_type, handler in self.input_mappings.items():
|
||||
if input_type in current_state:
|
||||
current_value = current_state[input_type]
|
||||
prev_value = prev_state.get(input_type, 0)
|
||||
|
||||
# 处理按钮按下/释放
|
||||
if isinstance(current_value, (int, float)):
|
||||
if current_value > 0.5 and prev_value <= 0.5:
|
||||
handler(controller_id, 'press', current_value)
|
||||
elif current_value <= 0.5 and prev_value > 0.5:
|
||||
handler(controller_id, 'release', current_value)
|
||||
elif current_value > 0.5:
|
||||
handler(controller_id, 'hold', current_value)
|
||||
|
||||
# 处理触摸板
|
||||
elif isinstance(current_value, tuple) and len(current_value) == 2:
|
||||
if current_value != prev_value:
|
||||
handler(controller_id, 'move', current_value)
|
||||
|
||||
def _handle_trigger(self, controller_id, action, value):
|
||||
"""处理扳机输入"""
|
||||
if action == 'press':
|
||||
print(f"控制器 {controller_id} 扳机按下 (强度: {value:.2f})")
|
||||
self._try_grab_object(controller_id)
|
||||
elif action == 'release':
|
||||
print(f"控制器 {controller_id} 扳机释放")
|
||||
self._try_release_object(controller_id)
|
||||
|
||||
def _handle_grip(self, controller_id, action, value):
|
||||
"""处理握持输入"""
|
||||
if action == 'press':
|
||||
print(f"控制器 {controller_id} 握持按下 (强度: {value:.2f})")
|
||||
self._toggle_interaction_mode(controller_id)
|
||||
elif action == 'release':
|
||||
print(f"控制器 {controller_id} 握持释放")
|
||||
|
||||
def _handle_touchpad(self, controller_id, action, value):
|
||||
"""处理触摸板输入"""
|
||||
if action == 'move':
|
||||
x, y = value
|
||||
print(f"控制器 {controller_id} 触摸板: ({x:.2f}, {y:.2f})")
|
||||
|
||||
# 根据触摸板位置执行不同操作
|
||||
if abs(x) > 0.7: # 左右滑动
|
||||
self._handle_horizontal_swipe(controller_id, x)
|
||||
elif abs(y) > 0.7: # 上下滑动
|
||||
self._handle_vertical_swipe(controller_id, y)
|
||||
|
||||
def _handle_menu(self, controller_id, action, value):
|
||||
"""处理菜单按钮"""
|
||||
if action == 'press':
|
||||
print(f"控制器 {controller_id} 菜单按钮按下")
|
||||
self._show_vr_menu(controller_id)
|
||||
|
||||
def _handle_system(self, controller_id, action, value):
|
||||
"""处理系统按钮"""
|
||||
if action == 'press':
|
||||
print(f"控制器 {controller_id} 系统按钮按下")
|
||||
# 系统按钮通常由VR系统处理
|
||||
|
||||
def _handle_horizontal_swipe(self, controller_id, direction):
|
||||
"""处理水平滑动"""
|
||||
if direction > 0:
|
||||
print(f"控制器 {controller_id} 右滑")
|
||||
self._switch_tool(controller_id, 'next')
|
||||
else:
|
||||
print(f"控制器 {controller_id} 左滑")
|
||||
self._switch_tool(controller_id, 'prev')
|
||||
|
||||
def _handle_vertical_swipe(self, controller_id, direction):
|
||||
"""处理垂直滑动"""
|
||||
if direction > 0:
|
||||
print(f"控制器 {controller_id} 上滑")
|
||||
self._zoom_in(controller_id)
|
||||
else:
|
||||
print(f"控制器 {controller_id} 下滑")
|
||||
self._zoom_out(controller_id)
|
||||
|
||||
def _try_grab_object(self, controller_id):
|
||||
"""尝试抓取对象"""
|
||||
if controller_id not in self.controllers:
|
||||
return
|
||||
|
||||
# 获取控制器射线
|
||||
ray = self._get_controller_ray(controller_id)
|
||||
if not ray:
|
||||
return
|
||||
|
||||
# 执行射线检测
|
||||
hit_object = self._raycast_from_controller(controller_id)
|
||||
if hit_object:
|
||||
self.selected_object = hit_object
|
||||
controller_pose = self.controllers[controller_id].get('pose')
|
||||
if controller_pose:
|
||||
# 计算抓取偏移
|
||||
object_pos = hit_object.getPos()
|
||||
controller_pos = controller_pose.getTranslate()
|
||||
self.grab_offset = object_pos - controller_pos
|
||||
|
||||
print(f"抓取对象: {hit_object.getName()}")
|
||||
|
||||
# 发送抓取事件
|
||||
self.world.event_handler.messenger.send('vr-object-grabbed', [hit_object, controller_id])
|
||||
|
||||
def _try_release_object(self, controller_id):
|
||||
"""尝试释放对象"""
|
||||
if self.selected_object:
|
||||
print(f"释放对象: {self.selected_object.getName()}")
|
||||
|
||||
# 发送释放事件
|
||||
self.world.event_handler.messenger.send('vr-object-released', [self.selected_object, controller_id])
|
||||
|
||||
self.selected_object = None
|
||||
self.grab_offset = Vec3(0, 0, 0)
|
||||
|
||||
def _raycast_from_controller(self, controller_id):
|
||||
"""从控制器发射射线检测"""
|
||||
if controller_id not in self.controllers:
|
||||
return None
|
||||
|
||||
controller_pose = self.controllers[controller_id].get('pose')
|
||||
if not controller_pose:
|
||||
return None
|
||||
|
||||
# 获取控制器位置和方向
|
||||
controller_pos = controller_pose.getTranslate()
|
||||
controller_forward = controller_pose.getQuat().getForward()
|
||||
|
||||
# 创建射线
|
||||
ray = CollisionRay()
|
||||
ray.setOrigin(controller_pos)
|
||||
ray.setDirection(controller_forward)
|
||||
|
||||
# 执行碰撞检测
|
||||
traverser = self.world.cTrav if hasattr(self.world, 'cTrav') else None
|
||||
if not traverser:
|
||||
return None
|
||||
|
||||
handler = CollisionHandlerQueue()
|
||||
collision_node = CollisionNode('vr_controller_ray')
|
||||
collision_node.addSolid(ray)
|
||||
|
||||
ray_np = self.world.render.attachNewNode(collision_node)
|
||||
traverser.addCollider(ray_np, handler)
|
||||
|
||||
# 遍历碰撞
|
||||
traverser.traverse(self.world.render)
|
||||
|
||||
# 清理
|
||||
ray_np.removeNode()
|
||||
|
||||
# 返回最近的碰撞对象
|
||||
if handler.getNumEntries() > 0:
|
||||
handler.sortEntries()
|
||||
entry = handler.getEntry(0)
|
||||
return entry.getIntoNodePath()
|
||||
|
||||
return None
|
||||
|
||||
def _get_controller_ray(self, controller_id):
|
||||
"""获取控制器射线"""
|
||||
return self.controller_rays.get(controller_id)
|
||||
|
||||
def _setup_controller_visualization(self):
|
||||
"""设置控制器可视化"""
|
||||
print("设置控制器可视化...")
|
||||
|
||||
# 为每个控制器创建可视化节点
|
||||
for controller_id in self.controllers:
|
||||
self._create_controller_model(controller_id)
|
||||
|
||||
def _create_controller_model(self, controller_id):
|
||||
"""创建控制器模型"""
|
||||
# 创建简单的控制器模型(立方体)
|
||||
from panda3d.core import CardMaker
|
||||
|
||||
cm = CardMaker(f"controller_{controller_id}")
|
||||
cm.setFrame(-0.05, 0.05, -0.05, 0.05)
|
||||
|
||||
controller_node = self.world.render.attachNewNode(cm.generate())
|
||||
controller_node.setColor(0.2, 0.8, 1.0, 0.8)
|
||||
controller_node.setScale(0.1, 0.2, 0.05)
|
||||
|
||||
self.controller_nodes[controller_id] = controller_node
|
||||
|
||||
# 创建控制器射线可视化
|
||||
self._create_controller_ray_visual(controller_id)
|
||||
|
||||
def _create_controller_ray_visual(self, controller_id):
|
||||
"""创建控制器射线可视化"""
|
||||
from panda3d.core import LineSegs
|
||||
|
||||
# 创建射线线段
|
||||
lines = LineSegs()
|
||||
lines.setColor(1, 0, 0, 0.5)
|
||||
lines.moveTo(0, 0, 0)
|
||||
lines.drawTo(0, 2, 0) # 2米长的射线
|
||||
|
||||
ray_node = self.world.render.attachNewNode(lines.create())
|
||||
ray_node.setRenderModeWireframe()
|
||||
ray_node.hide() # 默认隐藏
|
||||
|
||||
self.controller_rays[controller_id] = ray_node
|
||||
|
||||
def _update_controller_visualization(self, controller_id, pose):
|
||||
"""更新控制器可视化"""
|
||||
if controller_id in self.controller_nodes:
|
||||
node = self.controller_nodes[controller_id]
|
||||
node.setMat(pose)
|
||||
|
||||
if controller_id in self.controller_rays:
|
||||
ray_node = self.controller_rays[controller_id]
|
||||
ray_node.setMat(pose)
|
||||
|
||||
def _cleanup_controller_visualization(self):
|
||||
"""清理控制器可视化"""
|
||||
for node in self.controller_nodes.values():
|
||||
node.removeNode()
|
||||
|
||||
for ray in self.controller_rays.values():
|
||||
ray.removeNode()
|
||||
|
||||
self.controller_nodes.clear()
|
||||
self.controller_rays.clear()
|
||||
|
||||
def _process_gestures(self):
|
||||
"""处理手势识别"""
|
||||
# 简单的手势识别逻辑
|
||||
# 这里可以实现更复杂的手势识别算法
|
||||
pass
|
||||
|
||||
def _process_interactions(self):
|
||||
"""处理交互逻辑"""
|
||||
# 如果有选中的对象,更新其位置
|
||||
if self.selected_object:
|
||||
self._update_grabbed_object()
|
||||
|
||||
def _update_grabbed_object(self):
|
||||
"""更新被抓取对象的位置"""
|
||||
if not self.selected_object:
|
||||
return
|
||||
|
||||
# 找到抓取该对象的控制器
|
||||
grabbing_controller = None
|
||||
for controller_id, controller_state in self.controllers.items():
|
||||
if controller_state.get('trigger', 0) > 0.5:
|
||||
grabbing_controller = controller_id
|
||||
break
|
||||
|
||||
if not grabbing_controller:
|
||||
return
|
||||
|
||||
# 更新对象位置
|
||||
controller_pose = self.controllers[grabbing_controller].get('pose')
|
||||
if controller_pose:
|
||||
controller_pos = controller_pose.getTranslate()
|
||||
new_pos = controller_pos + self.grab_offset
|
||||
self.selected_object.setPos(new_pos)
|
||||
|
||||
def _toggle_interaction_mode(self, controller_id):
|
||||
"""切换交互模式"""
|
||||
self.interaction_enabled = not self.interaction_enabled
|
||||
print(f"交互模式: {'启用' if self.interaction_enabled else '禁用'}")
|
||||
|
||||
def _show_vr_menu(self, controller_id):
|
||||
"""显示VR菜单"""
|
||||
print(f"显示VR菜单 (控制器 {controller_id})")
|
||||
# 这里可以实现VR菜单显示逻辑
|
||||
pass
|
||||
|
||||
def _switch_tool(self, controller_id, direction):
|
||||
"""切换工具"""
|
||||
print(f"切换工具: {direction} (控制器 {controller_id})")
|
||||
# 这里可以实现工具切换逻辑
|
||||
pass
|
||||
|
||||
def _zoom_in(self, controller_id):
|
||||
"""放大"""
|
||||
print(f"放大 (控制器 {controller_id})")
|
||||
# 实现放大逻辑
|
||||
pass
|
||||
|
||||
def _zoom_out(self, controller_id):
|
||||
"""缩小"""
|
||||
print(f"缩小 (控制器 {controller_id})")
|
||||
# 实现缩小逻辑
|
||||
pass
|
||||
|
||||
def show_controller_rays(self, show=True):
|
||||
"""显示/隐藏控制器射线"""
|
||||
for ray in self.controller_rays.values():
|
||||
if show:
|
||||
ray.show()
|
||||
else:
|
||||
ray.hide()
|
||||
|
||||
def get_controller_state(self, controller_id):
|
||||
"""获取控制器状态"""
|
||||
return self.controllers.get(controller_id, {})
|
||||
|
||||
def get_all_controllers(self):
|
||||
"""获取所有控制器"""
|
||||
return list(self.controllers.keys())
|
||||
|
||||
def set_gesture_enabled(self, enabled):
|
||||
"""设置手势识别启用状态"""
|
||||
self.gesture_enabled = enabled
|
||||
print(f"手势识别: {'启用' if enabled else '禁用'}")
|
||||
|
||||
def set_interaction_enabled(self, enabled):
|
||||
"""设置交互启用状态"""
|
||||
self.interaction_enabled = enabled
|
||||
print(f"VR交互: {'启用' if enabled else '禁用'}")
|
||||
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交互管理器已清理")
|
||||
1706
core/vr_manager.py
1706
core/vr_manager.py
File diff suppressed because it is too large
Load Diff
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}手柄可视化已清理")
|
||||
@ -1,374 +0,0 @@
|
||||
# VR + ALVR 串流实现指南
|
||||
|
||||
## 🎯 概述
|
||||
|
||||
本指南详细介绍了如何在Panda3D引擎中实现VR支持,并通过ALVR串流到Quest等VR头显设备。
|
||||
|
||||
## 🏗️ 系统架构
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **VRManager** (`core/vr_manager.py`)
|
||||
- 负责VR系统初始化
|
||||
- 管理立体渲染(左右眼)
|
||||
- 处理VR设备跟踪
|
||||
- 集成OpenVR接口
|
||||
|
||||
2. **VRInputHandler** (`core/vr_input_handler.py`)
|
||||
- 处理VR控制器输入
|
||||
- 手势识别系统
|
||||
- 交互逻辑处理
|
||||
|
||||
3. **ALVRStreamer** (`core/alvr_streamer.py`)
|
||||
- ALVR服务器通信
|
||||
- 视频流编码和传输
|
||||
- 性能监控
|
||||
|
||||
4. **VRControlPanel** (`ui/vr_control_panel.py`)
|
||||
- VR系统GUI控制界面
|
||||
- 实时状态监控
|
||||
- 参数调整面板
|
||||
|
||||
## 🔧 安装和配置
|
||||
|
||||
### 1. 系统依赖
|
||||
|
||||
```bash
|
||||
# 安装Python依赖
|
||||
pip install -r requirements/vr-requirements.txt
|
||||
|
||||
# 安装OpenVR运行时
|
||||
# Windows: 下载并安装SteamVR
|
||||
# Linux:
|
||||
sudo apt-get install steam
|
||||
# 然后在Steam中安装SteamVR
|
||||
```
|
||||
|
||||
### 2. ALVR服务器设置
|
||||
|
||||
```bash
|
||||
# 下载ALVR服务器
|
||||
# 从 https://github.com/alvr-org/ALVR 下载最新版本
|
||||
|
||||
# Windows
|
||||
# 1. 解压到 C:\ALVR\
|
||||
# 2. 运行 alvr_server.exe
|
||||
|
||||
# Linux
|
||||
# 1. 解压到 /usr/local/bin/
|
||||
# 2. 运行 alvr_server
|
||||
```
|
||||
|
||||
### 3. Quest设备配置
|
||||
|
||||
```bash
|
||||
# 1. 在Quest上安装ALVR客户端
|
||||
# 2. 启用开发者模式
|
||||
# 3. 连接到同一WiFi网络
|
||||
# 4. 配置防火墙允许ALVR通信(端口9943-9944)
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 快速启动
|
||||
|
||||
```python
|
||||
# 在主程序中
|
||||
world = MyWorld()
|
||||
|
||||
# 一键启用VR模式
|
||||
if world.enableVRMode():
|
||||
print("VR模式已启用")
|
||||
|
||||
# 检查VR状态
|
||||
status = world.getVRStatus()
|
||||
print(f"VR设备: {status['vr_info']}")
|
||||
print(f"ALVR连接: {status['alvr_connected']}")
|
||||
print(f"串流状态: {status['alvr_streaming']}")
|
||||
```
|
||||
|
||||
### 详细控制
|
||||
|
||||
```python
|
||||
# 分步骤启动VR
|
||||
world = MyWorld()
|
||||
|
||||
# 1. 初始化VR系统
|
||||
if world.initializeVR():
|
||||
print("VR系统初始化成功")
|
||||
|
||||
# 2. 启动VR输入处理
|
||||
world.startVRInput()
|
||||
|
||||
# 3. 显示控制器射线
|
||||
world.showControllerRays(True)
|
||||
|
||||
# 4. 初始化ALVR串流
|
||||
if world.initializeALVR():
|
||||
print("ALVR初始化成功")
|
||||
|
||||
# 5. 设置串流质量
|
||||
world.setALVRStreamQuality(
|
||||
width=2880,
|
||||
height=1700,
|
||||
fps=72,
|
||||
bitrate=150
|
||||
)
|
||||
|
||||
# 6. 开始串流
|
||||
world.startALVRStreaming()
|
||||
```
|
||||
|
||||
### GUI控制界面
|
||||
|
||||
```python
|
||||
# 创建VR控制面板
|
||||
from ui.vr_control_panel import VRControlPanel
|
||||
|
||||
vr_panel = VRControlPanel(world)
|
||||
vr_panel.show()
|
||||
|
||||
# 面板功能:
|
||||
# - 一键启用/禁用VR
|
||||
# - ALVR串流控制
|
||||
# - 质量参数调整
|
||||
# - 实时性能监控
|
||||
# - 控制器状态显示
|
||||
```
|
||||
|
||||
## 🎮 VR交互功能
|
||||
|
||||
### 控制器输入处理
|
||||
|
||||
```python
|
||||
# 获取控制器状态
|
||||
controllers = world.getAllControllers()
|
||||
for controller_id in controllers:
|
||||
state = world.getControllerState(controller_id)
|
||||
|
||||
# 检查扳机按下
|
||||
if state['trigger'] > 0.5:
|
||||
print(f"控制器 {controller_id} 扳机按下")
|
||||
|
||||
# 检查触摸板输入
|
||||
touchpad_x, touchpad_y = state['touchpad']
|
||||
if abs(touchpad_x) > 0.5:
|
||||
print(f"触摸板水平滑动: {touchpad_x}")
|
||||
```
|
||||
|
||||
### 对象抓取和操作
|
||||
|
||||
```python
|
||||
# VR输入处理器自动处理对象抓取
|
||||
# 扳机按下时自动检测和抓取对象
|
||||
# 扳机释放时自动释放对象
|
||||
|
||||
# 监听VR事件
|
||||
world.event_handler.accept('vr-object-grabbed', onObjectGrabbed)
|
||||
world.event_handler.accept('vr-object-released', onObjectReleased)
|
||||
|
||||
def onObjectGrabbed(object_node, controller_id):
|
||||
print(f"抓取对象: {object_node.getName()}")
|
||||
|
||||
def onObjectReleased(object_node, controller_id):
|
||||
print(f"释放对象: {object_node.getName()}")
|
||||
```
|
||||
|
||||
### 触觉反馈
|
||||
|
||||
```python
|
||||
# 发送触觉反馈
|
||||
world.sendHapticFeedback(
|
||||
controller_id=0,
|
||||
duration=0.5, # 持续时间(秒)
|
||||
intensity=0.8 # 强度(0-1)
|
||||
)
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 渲染优化
|
||||
|
||||
```python
|
||||
# 调整VR渲染质量
|
||||
vr_manager = world.vr_manager
|
||||
vr_manager.render_scale = 1.0 # 渲染缩放 (0.5-2.0)
|
||||
|
||||
# 设置多重采样抗锯齿
|
||||
fb_props = FrameBufferProperties()
|
||||
fb_props.setMultisamples(4) # 4x MSAA
|
||||
```
|
||||
|
||||
### 网络优化
|
||||
|
||||
```python
|
||||
# 优化ALVR串流参数
|
||||
world.setALVRStreamQuality(
|
||||
width=2160, # 降低分辨率提高性能
|
||||
height=1200,
|
||||
fps=60, # 降低帧率减少延迟
|
||||
bitrate=100 # 降低比特率适应网络带宽
|
||||
)
|
||||
```
|
||||
|
||||
### 系统资源监控
|
||||
|
||||
```python
|
||||
# 获取性能状态
|
||||
streaming_status = world.getALVRStreamingStatus()
|
||||
print(f"串流FPS: {streaming_status['fps']}")
|
||||
print(f"延迟: {streaming_status['latency']} ms")
|
||||
print(f"分辨率: {streaming_status['resolution']}")
|
||||
```
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **VR系统初始化失败**
|
||||
```
|
||||
错误: OpenVR初始化失败
|
||||
解决: 确保SteamVR已安装并运行
|
||||
```
|
||||
|
||||
2. **ALVR连接失败**
|
||||
```
|
||||
错误: 无法连接到ALVR服务器
|
||||
解决:
|
||||
- 检查ALVR服务器是否运行
|
||||
- 确认防火墙设置
|
||||
- 检查网络连接
|
||||
```
|
||||
|
||||
3. **串流质量问题**
|
||||
```
|
||||
问题: 画面卡顿或延迟高
|
||||
解决:
|
||||
- 降低分辨率和帧率
|
||||
- 检查WiFi信号强度
|
||||
- 调整比特率设置
|
||||
```
|
||||
|
||||
### 调试命令
|
||||
|
||||
```python
|
||||
# 启用调试模式
|
||||
world.vr_manager.debug_mode = True
|
||||
|
||||
# 获取详细状态信息
|
||||
vr_info = world.getVRInfo()
|
||||
print(f"VR设备信息: {vr_info}")
|
||||
|
||||
# 检查网络连接
|
||||
if world.isALVRConnected():
|
||||
print("ALVR连接正常")
|
||||
else:
|
||||
print("ALVR连接失败")
|
||||
```
|
||||
|
||||
## 🎯 高级功能
|
||||
|
||||
### 自定义VR交互
|
||||
|
||||
```python
|
||||
# 创建自定义VR交互逻辑
|
||||
class CustomVRInteraction:
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
|
||||
def handleCustomGesture(self, gesture_data):
|
||||
# 处理自定义手势
|
||||
pass
|
||||
|
||||
def createVRMenu(self, controller_id):
|
||||
# 在VR中创建3D菜单
|
||||
pass
|
||||
```
|
||||
|
||||
### 多用户VR支持
|
||||
|
||||
```python
|
||||
# 支持多个VR用户
|
||||
class MultiUserVRManager:
|
||||
def __init__(self):
|
||||
self.vr_users = {}
|
||||
|
||||
def addVRUser(self, user_id, vr_manager):
|
||||
self.vr_users[user_id] = vr_manager
|
||||
|
||||
def syncVRUsers(self):
|
||||
# 同步多用户VR状态
|
||||
pass
|
||||
```
|
||||
|
||||
## 📋 配置文件
|
||||
|
||||
### VR配置 (`config/vr_config.yaml`)
|
||||
|
||||
```yaml
|
||||
vr:
|
||||
enabled: true
|
||||
render_scale: 1.0
|
||||
tracking_space: "standing"
|
||||
|
||||
alvr:
|
||||
server_ip: "127.0.0.1"
|
||||
server_port: 9943
|
||||
streaming_port: 9944
|
||||
|
||||
video:
|
||||
width: 2880
|
||||
height: 1700
|
||||
fps: 72
|
||||
bitrate: 150
|
||||
codec: "h264"
|
||||
|
||||
audio:
|
||||
enabled: true
|
||||
sample_rate: 48000
|
||||
channels: 2
|
||||
```
|
||||
|
||||
## 🚀 部署建议
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 创建VR开发环境
|
||||
python -m venv vr_env
|
||||
source vr_env/bin/activate
|
||||
pip install -r requirements/vr-requirements.txt
|
||||
|
||||
# 启动开发服务器
|
||||
python main.py --vr-mode
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```bash
|
||||
# 优化生产部署
|
||||
# 1. 使用专用VR计算机
|
||||
# 2. 配置高性能网络
|
||||
# 3. 启用GPU加速
|
||||
# 4. 监控系统性能
|
||||
```
|
||||
|
||||
## 📚 参考资料
|
||||
|
||||
- [OpenVR API文档](https://github.com/ValveSoftware/openvr/wiki/API-Documentation)
|
||||
- [ALVR项目主页](https://github.com/alvr-org/ALVR)
|
||||
- [Panda3D VR指南](https://docs.panda3d.org/1.10/python/programming/render-to-texture/index)
|
||||
- [Quest开发者文档](https://developer.oculus.com/documentation/quest/)
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎贡献代码和改进建议!请遵循以下步骤:
|
||||
|
||||
1. Fork此仓库
|
||||
2. 创建功能分支
|
||||
3. 提交代码修改
|
||||
4. 创建Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用MIT许可证。详见LICENSE文件。
|
||||
253
demo/VR测试说明.md
253
demo/VR测试说明.md
@ -1,253 +0,0 @@
|
||||
# VR测试说明
|
||||
|
||||
## 📋 测试条件说明
|
||||
|
||||
### 🎮 VR测试的两种模式
|
||||
|
||||
#### 1. 模拟模式(推荐用于开发和调试)
|
||||
- **无需VR硬件**
|
||||
- **无需SteamVR**
|
||||
- **无需ALVR服务器**
|
||||
- 自动启用,当真实VR不可用时
|
||||
- 模拟头盔和控制器追踪数据
|
||||
- 立体渲染显示到窗口
|
||||
- 适用于功能测试和开发
|
||||
|
||||
#### 2. 真实VR模式(需要完整VR设备)
|
||||
- **需要VR头盔**
|
||||
- **需要SteamVR运行**
|
||||
- **需要ALVR服务器(Quest无线)**
|
||||
- 真实的6DOF追踪
|
||||
- 立体渲染到VR头盔
|
||||
- 完整的VR交互体验
|
||||
|
||||
## 🔧 软件要求
|
||||
|
||||
### 必需依赖
|
||||
```bash
|
||||
# 安装VR相关依赖
|
||||
pip install -r requirements/vr-requirements.txt
|
||||
|
||||
# 主要包含:
|
||||
# - openvr>=1.26.7
|
||||
# - psutil>=5.9.0
|
||||
# - numpy>=1.21.0
|
||||
# - Pillow>=9.0.1
|
||||
```
|
||||
|
||||
### 系统要求
|
||||
- Python 3.8+
|
||||
- Panda3D
|
||||
- PyQt5
|
||||
- OpenGL支持的显卡
|
||||
|
||||
## 🎮 硬件要求(完整VR模式)
|
||||
|
||||
### 最低硬件要求
|
||||
- **显卡**: GTX 1060 / RX 580 或更好
|
||||
- **内存**: 8GB RAM
|
||||
- **处理器**: Intel i5-4590 / AMD FX 8350 或更好
|
||||
- **USB**: 至少1个USB 3.0端口
|
||||
|
||||
### 支持的VR头盔
|
||||
- **Quest 2/3**: 有线(USB-C)或无线(ALVR)
|
||||
- **Valve Index**: DisplayPort + USB 3.0
|
||||
- **HTC Vive**: HDMI + USB 3.0
|
||||
- **其他OpenVR兼容设备**
|
||||
|
||||
## 🌐 连接方式
|
||||
|
||||
### 有线连接
|
||||
1. **Quest 2/3**: USB-C数据线连接PC
|
||||
2. **Valve Index**: DisplayPort + USB 3.0
|
||||
3. **HTC Vive**: HDMI + USB 3.0
|
||||
|
||||
### 无线连接(推荐Quest)
|
||||
1. **下载ALVR服务器**: [GitHub](https://github.com/alvr-org/ALVR)
|
||||
2. **在PC上运行ALVR服务器**
|
||||
3. **在Quest上安装ALVR客户端**
|
||||
4. **连接到同一Wi-Fi网络(5GHz推荐)**
|
||||
|
||||
## 🔄 启动顺序
|
||||
|
||||
### 完整VR模式启动顺序
|
||||
1. **连接VR头盔**到PC
|
||||
2. **启动SteamVR**
|
||||
3. **(可选)启动ALVR服务器**(Quest无线)
|
||||
4. **运行VR测试脚本**
|
||||
|
||||
### 模拟模式启动顺序
|
||||
1. **直接运行VR测试脚本**
|
||||
2. **系统会自动切换到模拟模式**
|
||||
|
||||
## 📊 测试项目说明
|
||||
|
||||
### 1. 基本VR功能测试
|
||||
- VR系统初始化
|
||||
- 控制器检测
|
||||
- 追踪数据获取
|
||||
- 立体渲染测试
|
||||
|
||||
### 2. VR模拟模式测试
|
||||
- 强制启用模拟模式
|
||||
- 模拟数据更新
|
||||
- 控制器输入模拟
|
||||
- 立体渲染显示
|
||||
|
||||
### 3. ALVR串流测试
|
||||
- ALVR服务器连接
|
||||
- 串流质量设置
|
||||
- 触觉反馈测试
|
||||
- 串流开关控制
|
||||
|
||||
### 4. VR GUI控制面板
|
||||
- 图形界面控制
|
||||
- 实时状态监控
|
||||
- 参数调整
|
||||
- 系统开关
|
||||
|
||||
### 5. VR交互功能测试
|
||||
- 控制器射线显示
|
||||
- 手势识别
|
||||
- 对象交互
|
||||
- 输入处理
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 开发和调试(推荐)
|
||||
```bash
|
||||
# 直接运行,会自动使用模拟模式
|
||||
python vr_test.py
|
||||
|
||||
# 选择 "2. VR模拟模式测试"
|
||||
```
|
||||
|
||||
### 完整VR测试
|
||||
```bash
|
||||
# 确保VR设备已连接并启动SteamVR
|
||||
# 然后运行
|
||||
python vr_test.py
|
||||
|
||||
# 选择 "1. 基本VR功能测试"
|
||||
```
|
||||
|
||||
## 💡 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. OpenVR初始化失败
|
||||
**现象**: 显示"OpenVR初始化失败"
|
||||
**解决方案**:
|
||||
- 检查SteamVR是否运行
|
||||
- 确认VR头盔被系统识别
|
||||
- 重启SteamVR和头盔
|
||||
- 系统会自动切换到模拟模式
|
||||
|
||||
#### 2. 控制器未检测到
|
||||
**现象**: 控制器显示"未连接"
|
||||
**解决方案**:
|
||||
- 确保控制器已配对
|
||||
- 检查控制器电量
|
||||
- 在SteamVR中重新配对
|
||||
- 模拟模式下会显示模拟控制器
|
||||
|
||||
#### 3. ALVR连接失败
|
||||
**现象**: ALVR串流测试失败
|
||||
**解决方案**:
|
||||
- 确保ALVR服务器正在运行
|
||||
- 检查Quest上的ALVR客户端
|
||||
- 确认PC和Quest在同一网络
|
||||
- 使用5GHz Wi-Fi网络
|
||||
|
||||
#### 4. 性能问题
|
||||
**现象**: 帧率低或卡顿
|
||||
**解决方案**:
|
||||
- 降低渲染分辨率
|
||||
- 关闭不必要的后台程序
|
||||
- 检查显卡驱动更新
|
||||
- 使用模拟模式进行开发
|
||||
|
||||
### 日志分析
|
||||
测试过程中的详细日志可以帮助诊断问题:
|
||||
- `✓` 表示成功
|
||||
- `⚠` 表示警告(非致命)
|
||||
- `✗` 表示失败
|
||||
- `🔧` 表示配置或调试信息
|
||||
|
||||
## 📈 性能优化建议
|
||||
|
||||
### 开发阶段
|
||||
1. **使用模拟模式**进行功能开发
|
||||
2. **降低渲染分辨率**提高帧率
|
||||
3. **关闭不必要的特效**
|
||||
4. **使用性能分析工具**
|
||||
|
||||
### 生产部署
|
||||
1. **使用真实VR模式**
|
||||
2. **根据硬件配置调整质量**
|
||||
3. **启用异步时间扭曲**
|
||||
4. **优化渲染管线**
|
||||
|
||||
## 🔍 调试技巧
|
||||
|
||||
### 1. 检查VR系统状态
|
||||
```python
|
||||
# 在代码中添加调试信息
|
||||
vr_info = world.getVRInfo()
|
||||
print(f"VR模式: {vr_info['mode']}")
|
||||
print(f"模拟模式: {vr_info['simulation_mode']}")
|
||||
```
|
||||
|
||||
### 2. 监控性能
|
||||
```python
|
||||
# 获取VR系统状态
|
||||
status = world.getVRStatus()
|
||||
print(f"VR启用: {status['vr_enabled']}")
|
||||
print(f"控制器数量: {len(status['controllers'])}")
|
||||
```
|
||||
|
||||
### 3. 测试特定功能
|
||||
```python
|
||||
# 强制启用模拟模式
|
||||
world.vr_manager.enable_simulation_mode()
|
||||
|
||||
# 更新模拟数据
|
||||
world.vr_manager.update_simulation_data('head_pose', new_data)
|
||||
```
|
||||
|
||||
## 📚 API参考
|
||||
|
||||
### VR系统控制
|
||||
- `world.initializeVR()` - 初始化VR系统
|
||||
- `world.shutdownVR()` - 关闭VR系统
|
||||
- `world.isVREnabled()` - 检查VR状态
|
||||
- `world.getVRInfo()` - 获取VR信息
|
||||
|
||||
### 模拟模式
|
||||
- `world.vr_manager.enable_simulation_mode()` - 启用模拟模式
|
||||
- `world.vr_manager.get_simulation_data()` - 获取模拟数据
|
||||
- `world.vr_manager.update_simulation_data()` - 更新模拟数据
|
||||
|
||||
### ALVR串流
|
||||
- `world.initializeALVR()` - 初始化ALVR
|
||||
- `world.startALVRStreaming()` - 开始串流
|
||||
- `world.stopALVRStreaming()` - 停止串流
|
||||
- `world.setALVRStreamQuality()` - 设置质量
|
||||
|
||||
### VR输入
|
||||
- `world.startVRInput()` - 启动输入处理
|
||||
- `world.showControllerRays()` - 显示控制器射线
|
||||
- `world.getAllControllers()` - 获取所有控制器
|
||||
- `world.getControllerState()` - 获取控制器状态
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
现在的VR测试系统具有以下优势:
|
||||
|
||||
1. **自动适应**: 自动在真实VR和模拟模式之间切换
|
||||
2. **开发友好**: 无需VR硬件即可开发和测试
|
||||
3. **完整功能**: 支持所有VR功能的测试
|
||||
4. **详细反馈**: 提供清晰的状态信息和错误提示
|
||||
5. **灵活配置**: 可根据需要调整各种参数
|
||||
|
||||
无论您是否拥有VR设备,都可以使用这个测试系统来验证VR功能的正确性。
|
||||
153
main.py
153
main.py
@ -19,9 +19,6 @@ from core.selection import SelectionSystem
|
||||
from core.event_handler import EventHandler
|
||||
from core.tool_manager import ToolManager
|
||||
from core.script_system import ScriptManager
|
||||
from core.vr_manager import VRManager
|
||||
from core.vr_input_handler import VRInputHandler
|
||||
from core.alvr_streamer import ALVRStreamer
|
||||
from gui.gui_manager import GUIManager
|
||||
from core.terrain_manager import TerrainManager
|
||||
from scene.scene_manager import SceneManager
|
||||
@ -83,10 +80,6 @@ class MyWorld(CoreWorld):
|
||||
# 初始化界面管理系统
|
||||
self.interface_manager = InterfaceManager(self)
|
||||
|
||||
# 初始化VR系统
|
||||
self.vr_manager = VRManager(self)
|
||||
self.vr_input_handler = VRInputHandler(self, self.vr_manager)
|
||||
self.alvr_streamer = ALVRStreamer(self, self.vr_manager)
|
||||
|
||||
# 启动脚本系统
|
||||
self.script_manager.start_system()
|
||||
@ -104,6 +97,15 @@ class MyWorld(CoreWorld):
|
||||
from core.collision_manager import CollisionManager
|
||||
self.collision_manager = CollisionManager(self)
|
||||
|
||||
# 初始化VR管理器
|
||||
try:
|
||||
from core.vr_manager import VRManager
|
||||
self.vr_manager = VRManager(self)
|
||||
print("✓ VR管理器初始化完成")
|
||||
except Exception as e:
|
||||
print(f"⚠ VR管理器初始化失败: {e}")
|
||||
self.vr_manager = None
|
||||
|
||||
# 调试选项
|
||||
self.debug_collision = False # 是否显示碰撞体
|
||||
|
||||
@ -606,143 +608,6 @@ class MyWorld(CoreWorld):
|
||||
def listAllScripts(self):
|
||||
"""列出所有脚本信息"""
|
||||
return self.script_manager.list_all_scripts()
|
||||
# ==================== VR系统功能代理 ====================
|
||||
|
||||
# VR系统控制方法 - 代理到vr_manager
|
||||
def initializeVR(self):
|
||||
"""初始化VR系统"""
|
||||
return self.vr_manager.initialize_vr()
|
||||
|
||||
def shutdownVR(self):
|
||||
"""关闭VR系统"""
|
||||
return self.vr_manager.shutdown_vr()
|
||||
|
||||
def isVREnabled(self):
|
||||
"""检查VR是否启用"""
|
||||
return self.vr_manager.is_vr_enabled()
|
||||
|
||||
def getVRInfo(self):
|
||||
"""获取VR系统信息"""
|
||||
return self.vr_manager.get_vr_info()
|
||||
|
||||
# VR输入处理方法 - 代理到vr_input_handler
|
||||
def startVRInput(self):
|
||||
"""启动VR输入处理"""
|
||||
return self.vr_input_handler.start_input_handling()
|
||||
|
||||
def stopVRInput(self):
|
||||
"""停止VR输入处理"""
|
||||
return self.vr_input_handler.stop_input_handling()
|
||||
|
||||
def showControllerRays(self, show=True):
|
||||
"""显示/隐藏控制器射线"""
|
||||
return self.vr_input_handler.show_controller_rays(show)
|
||||
|
||||
def getControllerState(self, controller_id):
|
||||
"""获取控制器状态"""
|
||||
return self.vr_input_handler.get_controller_state(controller_id)
|
||||
|
||||
def getAllControllers(self):
|
||||
"""获取所有控制器"""
|
||||
return self.vr_input_handler.get_all_controllers()
|
||||
|
||||
def setVRGestureEnabled(self, enabled):
|
||||
"""设置VR手势识别启用状态"""
|
||||
return self.vr_input_handler.set_gesture_enabled(enabled)
|
||||
|
||||
def setVRInteractionEnabled(self, enabled):
|
||||
"""设置VR交互启用状态"""
|
||||
return self.vr_input_handler.set_interaction_enabled(enabled)
|
||||
|
||||
# ALVR串流方法 - 代理到alvr_streamer
|
||||
def initializeALVR(self):
|
||||
"""初始化ALVR串流"""
|
||||
return self.alvr_streamer.initialize()
|
||||
|
||||
def startALVRStreaming(self):
|
||||
"""开始ALVR串流"""
|
||||
return self.alvr_streamer.start_streaming()
|
||||
|
||||
def stopALVRStreaming(self):
|
||||
"""停止ALVR串流"""
|
||||
return self.alvr_streamer.stop_streaming()
|
||||
|
||||
def getALVRStreamingStatus(self):
|
||||
"""获取ALVR串流状态"""
|
||||
return self.alvr_streamer.get_streaming_status()
|
||||
|
||||
def setALVRStreamQuality(self, width, height, fps, bitrate):
|
||||
"""设置ALVR串流质量"""
|
||||
return self.alvr_streamer.set_stream_quality(width, height, fps, bitrate)
|
||||
|
||||
def sendHapticFeedback(self, controller_id, duration, intensity):
|
||||
"""发送触觉反馈"""
|
||||
return self.alvr_streamer.send_haptic_feedback(controller_id, duration, intensity)
|
||||
|
||||
def shutdownALVR(self):
|
||||
"""关闭ALVR串流"""
|
||||
return self.alvr_streamer.shutdown()
|
||||
|
||||
def isALVRConnected(self):
|
||||
"""检查ALVR是否连接"""
|
||||
return self.alvr_streamer.is_connected()
|
||||
|
||||
def isALVRStreaming(self):
|
||||
"""检查ALVR是否在串流"""
|
||||
return self.alvr_streamer.is_streaming()
|
||||
|
||||
# 便捷方法
|
||||
def enableVRMode(self):
|
||||
"""启用VR模式(一键启动)"""
|
||||
print("启用VR模式...")
|
||||
|
||||
# 1. 初始化VR系统
|
||||
if not self.initializeVR():
|
||||
print("VR系统初始化失败")
|
||||
return False
|
||||
|
||||
# 2. 启动VR输入处理
|
||||
if not self.startVRInput():
|
||||
print("VR输入处理启动失败")
|
||||
return False
|
||||
|
||||
# 3. 初始化ALVR串流
|
||||
if self.initializeALVR():
|
||||
print("✓ ALVR串流已启用")
|
||||
# 自动开始串流
|
||||
self.startALVRStreaming()
|
||||
else:
|
||||
print("⚠ ALVR串流启用失败,但VR系统仍可用")
|
||||
|
||||
print("✓ VR模式已启用")
|
||||
return True
|
||||
|
||||
def disableVRMode(self):
|
||||
"""禁用VR模式(一键关闭)"""
|
||||
print("禁用VR模式...")
|
||||
|
||||
# 1. 停止ALVR串流
|
||||
self.stopALVRStreaming()
|
||||
self.shutdownALVR()
|
||||
|
||||
# 2. 停止VR输入处理
|
||||
self.stopVRInput()
|
||||
|
||||
# 3. 关闭VR系统
|
||||
self.shutdownVR()
|
||||
|
||||
print("✓ VR模式已禁用")
|
||||
|
||||
def getVRStatus(self):
|
||||
"""获取VR系统总体状态"""
|
||||
return {
|
||||
"vr_enabled": self.isVREnabled(),
|
||||
"vr_info": self.getVRInfo(),
|
||||
"controllers": self.getAllControllers(),
|
||||
"alvr_connected": self.isALVRConnected(),
|
||||
"alvr_streaming": self.isALVRStreaming(),
|
||||
"streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None
|
||||
}
|
||||
|
||||
def loadCesiumTileset(self,tileset_url,position=(0,0,0)):
|
||||
return self.scene_manager.load_cesium_tileset(tileset_url,position)
|
||||
|
||||
@ -98,3 +98,4 @@ webencodings==0.5.1
|
||||
xdg==5
|
||||
xkit==0.0.0
|
||||
zipp==1.0.0
|
||||
openvr==2.2.0
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
# VR功能依赖包
|
||||
# 用于支持VR显示和ALVR串流功能
|
||||
|
||||
# OpenVR Python库 - VR系统核心
|
||||
openvr>=1.26.7
|
||||
|
||||
# 网络和通信
|
||||
psutil>=5.9.0
|
||||
|
||||
# 数学和科学计算
|
||||
numpy>=1.21.0
|
||||
|
||||
# 图像处理
|
||||
Pillow>=9.0.1
|
||||
|
||||
# 现有核心依赖
|
||||
Panda3D>=1.10.15
|
||||
PyQt5>=5.15.9
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
@ -438,6 +438,17 @@ class MainWindow(QMainWindow):
|
||||
self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源')
|
||||
self.refreshAssetsAction.triggered.connect(self.refreshAssetsView)
|
||||
|
||||
# VR菜单
|
||||
self.vrMenu = menubar.addMenu('VR')
|
||||
self.enterVRAction = self.vrMenu.addAction('进入VR模式')
|
||||
self.exitVRAction = self.vrMenu.addAction('退出VR模式')
|
||||
self.vrMenu.addSeparator()
|
||||
self.vrStatusAction = self.vrMenu.addAction('VR状态')
|
||||
self.vrSettingsAction = self.vrMenu.addAction('VR设置')
|
||||
|
||||
# 初始状态下禁用退出VR选项
|
||||
self.exitVRAction.setEnabled(False)
|
||||
|
||||
# 帮助菜单
|
||||
self.helpMenu = menubar.addMenu('帮助')
|
||||
self.aboutAction = self.helpMenu.addAction('关于')
|
||||
@ -898,6 +909,12 @@ class MainWindow(QMainWindow):
|
||||
# self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
|
||||
# self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
|
||||
|
||||
# 连接VR菜单事件
|
||||
self.enterVRAction.triggered.connect(self.onEnterVR)
|
||||
self.exitVRAction.triggered.connect(self.onExitVR)
|
||||
self.vrStatusAction.triggered.connect(self.onShowVRStatus)
|
||||
self.vrSettingsAction.triggered.connect(self.onShowVRSettings)
|
||||
|
||||
|
||||
def onCreateCesiumView(self):
|
||||
if hasattr(self.world,'gui_manager') and self.world.gui_manager:
|
||||
@ -1845,13 +1862,21 @@ class MainWindow(QMainWindow):
|
||||
# 清理工具管理器中的进程
|
||||
if hasattr(self.world, 'tool_manager') and self.world.tool_manager:
|
||||
print("🧹 清理工具管理器进程...")
|
||||
self.world.tool_manager.cleanup_processes()
|
||||
if hasattr(self.world.tool_manager, 'cleanup_processes'):
|
||||
self.world.tool_manager.cleanup_processes()
|
||||
else:
|
||||
print("✓ 工具管理器无需清理进程")
|
||||
|
||||
# 停止更新定时器
|
||||
if hasattr(self, 'updateTimer') and self.updateTimer:
|
||||
self.updateTimer.stop()
|
||||
print("⏹️ 更新定时器已停止")
|
||||
|
||||
# 清理VR资源
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
print("🧹 清理VR资源...")
|
||||
self.world.vr_manager.cleanup()
|
||||
|
||||
# 清理Panda3D资源
|
||||
if hasattr(self, 'pandaWidget') and self.pandaWidget:
|
||||
print("🧹 清理Panda3D资源...")
|
||||
@ -2000,6 +2025,171 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "高度图地形创建失败!")
|
||||
|
||||
# ==================== VR事件处理 ====================
|
||||
|
||||
def onEnterVR(self):
|
||||
"""进入VR模式"""
|
||||
try:
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
success = self.world.vr_manager.enable_vr()
|
||||
if success:
|
||||
# 更新菜单状态
|
||||
self.enterVRAction.setEnabled(False)
|
||||
self.exitVRAction.setEnabled(True)
|
||||
QMessageBox.information(self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。")
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装")
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "VR管理器不可用!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"启用VR模式时发生错误:\n{str(e)}")
|
||||
|
||||
def onExitVR(self):
|
||||
"""退出VR模式"""
|
||||
try:
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
self.world.vr_manager.disable_vr()
|
||||
# 更新菜单状态
|
||||
self.enterVRAction.setEnabled(True)
|
||||
self.exitVRAction.setEnabled(False)
|
||||
QMessageBox.information(self, "成功", "已退出VR模式")
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "VR管理器不可用!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"退出VR模式时发生错误:\n{str(e)}")
|
||||
|
||||
def onShowVRStatus(self):
|
||||
"""显示VR状态"""
|
||||
try:
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
status = self.world.vr_manager.get_vr_status()
|
||||
|
||||
status_text = f"""VR系统状态:
|
||||
|
||||
可用性: {'✅ 可用' if status['available'] else '❌ 不可用'}
|
||||
初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'}
|
||||
启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'}
|
||||
渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}
|
||||
追踪设备数: {status['device_count']}
|
||||
|
||||
提示:
|
||||
- 如果VR不可用,请确保已安装SteamVR并连接VR头显
|
||||
- 如果OpenVR库未安装,请运行:pip install openvr
|
||||
"""
|
||||
|
||||
QMessageBox.information(self, "VR状态", status_text)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "VR管理器不可用!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"获取VR状态时发生错误:\n{str(e)}")
|
||||
|
||||
def onShowVRSettings(self):
|
||||
"""显示VR设置对话框"""
|
||||
try:
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
dialog = self.createVRSettingsDialog()
|
||||
dialog.exec_()
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "VR管理器不可用!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"打开VR设置时发生错误:\n{str(e)}")
|
||||
|
||||
def createVRSettingsDialog(self):
|
||||
"""创建VR设置对话框"""
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("VR设置")
|
||||
dialog.setModal(True)
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# VR状态显示
|
||||
status_group = QGroupBox("VR状态")
|
||||
status_layout = QVBoxLayout()
|
||||
|
||||
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
|
||||
status = self.world.vr_manager.get_vr_status()
|
||||
|
||||
available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}")
|
||||
available_label.setStyleSheet(f"color: {'green' if status['available'] else 'red'};")
|
||||
status_layout.addWidget(available_label)
|
||||
|
||||
enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}")
|
||||
enabled_label.setStyleSheet(f"color: {'green' if status['enabled'] else 'gray'};")
|
||||
status_layout.addWidget(enabled_label)
|
||||
|
||||
resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}")
|
||||
status_layout.addWidget(resolution_label)
|
||||
|
||||
status_group.setLayout(status_layout)
|
||||
layout.addWidget(status_group)
|
||||
|
||||
# 渲染设置
|
||||
render_group = QGroupBox("渲染设置")
|
||||
render_layout = QFormLayout()
|
||||
|
||||
# 渲染质量
|
||||
quality_combo = QComboBox()
|
||||
quality_combo.addItems(["低", "中", "高", "超高"])
|
||||
quality_combo.setCurrentText("高")
|
||||
render_layout.addRow("渲染质量:", quality_combo)
|
||||
|
||||
# 抗锯齿
|
||||
aa_combo = QComboBox()
|
||||
aa_combo.addItems(["无", "2x", "4x", "8x"])
|
||||
aa_combo.setCurrentText("4x")
|
||||
render_layout.addRow("抗锯齿:", aa_combo)
|
||||
|
||||
render_group.setLayout(render_layout)
|
||||
layout.addWidget(render_group)
|
||||
|
||||
# 性能设置
|
||||
perf_group = QGroupBox("性能设置")
|
||||
perf_layout = QFormLayout()
|
||||
|
||||
# 刷新率
|
||||
refresh_combo = QComboBox()
|
||||
refresh_combo.addItems(["72Hz", "90Hz", "120Hz", "144Hz"])
|
||||
refresh_combo.setCurrentText("90Hz")
|
||||
perf_layout.addRow("刷新率:", refresh_combo)
|
||||
|
||||
# 异步重投影
|
||||
async_check = QCheckBox("启用异步重投影")
|
||||
async_check.setChecked(True)
|
||||
perf_layout.addRow("", async_check)
|
||||
|
||||
perf_group.setLayout(perf_layout)
|
||||
layout.addWidget(perf_group)
|
||||
|
||||
# 按钮
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
apply_button = QPushButton("应用")
|
||||
ok_button = QPushButton("确定")
|
||||
cancel_button = QPushButton("取消")
|
||||
|
||||
button_layout.addWidget(apply_button)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(ok_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 连接信号
|
||||
apply_button.clicked.connect(lambda: self.applyVRSettings(dialog))
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
return dialog
|
||||
|
||||
def applyVRSettings(self, dialog):
|
||||
"""应用VR设置"""
|
||||
try:
|
||||
# 这里可以实现设置的保存和应用逻辑
|
||||
QMessageBox.information(dialog, "成功", "VR设置已应用!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}")
|
||||
|
||||
def setup_main_window(world,path = None):
|
||||
"""设置主窗口的便利函数"""
|
||||
app = QApplication.instance()
|
||||
|
||||
@ -1,412 +0,0 @@
|
||||
"""
|
||||
VR控制面板
|
||||
|
||||
提供VR系统的GUI控制界面
|
||||
包括VR启用/禁用、ALVR串流控制、性能监控等功能
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
|
||||
QPushButton, QLabel, QProgressBar, QSlider,
|
||||
QSpinBox, QCheckBox, QComboBox, QTextEdit,
|
||||
QGridLayout, QFrame, QSizePolicy)
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont, QColor, QPalette
|
||||
|
||||
|
||||
class VRControlPanel(QWidget):
|
||||
"""VR控制面板"""
|
||||
|
||||
# 信号定义
|
||||
vr_enabled = pyqtSignal(bool)
|
||||
alvr_streaming_changed = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, world, parent=None):
|
||||
super().__init__(parent)
|
||||
self.world = world
|
||||
self.setupUI()
|
||||
self.connectSignals()
|
||||
|
||||
# 设置更新定时器
|
||||
self.update_timer = QTimer()
|
||||
self.update_timer.timeout.connect(self.updateStatus)
|
||||
self.update_timer.start(1000) # 每秒更新一次
|
||||
|
||||
def setupUI(self):
|
||||
"""设置界面"""
|
||||
self.setWindowTitle("VR控制面板")
|
||||
self.setMinimumSize(400, 600)
|
||||
|
||||
# 主布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
# VR系统控制组
|
||||
vr_control_group = self.createVRControlGroup()
|
||||
main_layout.addWidget(vr_control_group)
|
||||
|
||||
# ALVR串流控制组
|
||||
alvr_control_group = self.createALVRControlGroup()
|
||||
main_layout.addWidget(alvr_control_group)
|
||||
|
||||
# 性能监控组
|
||||
performance_group = self.createPerformanceGroup()
|
||||
main_layout.addWidget(performance_group)
|
||||
|
||||
# 控制器状态组
|
||||
controller_group = self.createControllerGroup()
|
||||
main_layout.addWidget(controller_group)
|
||||
|
||||
# 日志输出组
|
||||
log_group = self.createLogGroup()
|
||||
main_layout.addWidget(log_group)
|
||||
|
||||
# 添加弹性空间
|
||||
main_layout.addStretch()
|
||||
|
||||
def createVRControlGroup(self):
|
||||
"""创建VR系统控制组"""
|
||||
group = QGroupBox("VR系统控制")
|
||||
layout = QVBoxLayout(group)
|
||||
|
||||
# 状态指示器
|
||||
self.vr_status_label = QLabel("VR状态: 未启用")
|
||||
self.vr_status_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
layout.addWidget(self.vr_status_label)
|
||||
|
||||
# 启用/禁用VR按钮
|
||||
button_layout = QHBoxLayout()
|
||||
self.enable_vr_button = QPushButton("启用VR")
|
||||
self.enable_vr_button.clicked.connect(self.enableVR)
|
||||
button_layout.addWidget(self.enable_vr_button)
|
||||
|
||||
self.disable_vr_button = QPushButton("禁用VR")
|
||||
self.disable_vr_button.clicked.connect(self.disableVR)
|
||||
self.disable_vr_button.setEnabled(False)
|
||||
button_layout.addWidget(self.disable_vr_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# VR设备信息
|
||||
self.vr_info_label = QLabel("VR设备: 未连接")
|
||||
layout.addWidget(self.vr_info_label)
|
||||
|
||||
# VR选项
|
||||
options_layout = QGridLayout()
|
||||
|
||||
# 控制器射线显示
|
||||
self.show_rays_checkbox = QCheckBox("显示控制器射线")
|
||||
self.show_rays_checkbox.stateChanged.connect(self.toggleControllerRays)
|
||||
options_layout.addWidget(self.show_rays_checkbox, 0, 0)
|
||||
|
||||
# 手势识别
|
||||
self.gesture_checkbox = QCheckBox("启用手势识别")
|
||||
self.gesture_checkbox.stateChanged.connect(self.toggleGestureRecognition)
|
||||
options_layout.addWidget(self.gesture_checkbox, 0, 1)
|
||||
|
||||
# VR交互
|
||||
self.interaction_checkbox = QCheckBox("启用VR交互")
|
||||
self.interaction_checkbox.setChecked(True)
|
||||
self.interaction_checkbox.stateChanged.connect(self.toggleVRInteraction)
|
||||
options_layout.addWidget(self.interaction_checkbox, 1, 0)
|
||||
|
||||
layout.addLayout(options_layout)
|
||||
|
||||
return group
|
||||
|
||||
def createALVRControlGroup(self):
|
||||
"""创建ALVR串流控制组"""
|
||||
group = QGroupBox("ALVR串流控制")
|
||||
layout = QVBoxLayout(group)
|
||||
|
||||
# 连接状态
|
||||
self.alvr_status_label = QLabel("ALVR状态: 未连接")
|
||||
self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
layout.addWidget(self.alvr_status_label)
|
||||
|
||||
# 串流控制按钮
|
||||
button_layout = QHBoxLayout()
|
||||
self.start_streaming_button = QPushButton("开始串流")
|
||||
self.start_streaming_button.clicked.connect(self.startStreaming)
|
||||
self.start_streaming_button.setEnabled(False)
|
||||
button_layout.addWidget(self.start_streaming_button)
|
||||
|
||||
self.stop_streaming_button = QPushButton("停止串流")
|
||||
self.stop_streaming_button.clicked.connect(self.stopStreaming)
|
||||
self.stop_streaming_button.setEnabled(False)
|
||||
button_layout.addWidget(self.stop_streaming_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 串流质量设置
|
||||
quality_layout = QGridLayout()
|
||||
|
||||
# 分辨率
|
||||
quality_layout.addWidget(QLabel("分辨率宽度:"), 0, 0)
|
||||
self.width_spinbox = QSpinBox()
|
||||
self.width_spinbox.setRange(1920, 4096)
|
||||
self.width_spinbox.setValue(2880)
|
||||
self.width_spinbox.setSuffix(" px")
|
||||
quality_layout.addWidget(self.width_spinbox, 0, 1)
|
||||
|
||||
quality_layout.addWidget(QLabel("分辨率高度:"), 1, 0)
|
||||
self.height_spinbox = QSpinBox()
|
||||
self.height_spinbox.setRange(1080, 2160)
|
||||
self.height_spinbox.setValue(1700)
|
||||
self.height_spinbox.setSuffix(" px")
|
||||
quality_layout.addWidget(self.height_spinbox, 1, 1)
|
||||
|
||||
# 帧率
|
||||
quality_layout.addWidget(QLabel("帧率:"), 2, 0)
|
||||
self.fps_spinbox = QSpinBox()
|
||||
self.fps_spinbox.setRange(60, 120)
|
||||
self.fps_spinbox.setValue(72)
|
||||
self.fps_spinbox.setSuffix(" fps")
|
||||
quality_layout.addWidget(self.fps_spinbox, 2, 1)
|
||||
|
||||
# 比特率
|
||||
quality_layout.addWidget(QLabel("比特率:"), 3, 0)
|
||||
self.bitrate_spinbox = QSpinBox()
|
||||
self.bitrate_spinbox.setRange(50, 500)
|
||||
self.bitrate_spinbox.setValue(150)
|
||||
self.bitrate_spinbox.setSuffix(" Mbps")
|
||||
quality_layout.addWidget(self.bitrate_spinbox, 3, 1)
|
||||
|
||||
layout.addLayout(quality_layout)
|
||||
|
||||
# 应用设置按钮
|
||||
self.apply_quality_button = QPushButton("应用质量设置")
|
||||
self.apply_quality_button.clicked.connect(self.applyQualitySettings)
|
||||
layout.addWidget(self.apply_quality_button)
|
||||
|
||||
return group
|
||||
|
||||
def createPerformanceGroup(self):
|
||||
"""创建性能监控组"""
|
||||
group = QGroupBox("性能监控")
|
||||
layout = QGridLayout(group)
|
||||
|
||||
# FPS显示
|
||||
layout.addWidget(QLabel("渲染FPS:"), 0, 0)
|
||||
self.fps_label = QLabel("0")
|
||||
self.fps_label.setStyleSheet("font-weight: bold; color: blue;")
|
||||
layout.addWidget(self.fps_label, 0, 1)
|
||||
|
||||
# 串流FPS
|
||||
layout.addWidget(QLabel("串流FPS:"), 1, 0)
|
||||
self.stream_fps_label = QLabel("0")
|
||||
self.stream_fps_label.setStyleSheet("font-weight: bold; color: green;")
|
||||
layout.addWidget(self.stream_fps_label, 1, 1)
|
||||
|
||||
# 延迟
|
||||
layout.addWidget(QLabel("延迟:"), 2, 0)
|
||||
self.latency_label = QLabel("0 ms")
|
||||
self.latency_label.setStyleSheet("font-weight: bold; color: orange;")
|
||||
layout.addWidget(self.latency_label, 2, 1)
|
||||
|
||||
# 性能进度条
|
||||
layout.addWidget(QLabel("CPU使用率:"), 3, 0)
|
||||
self.cpu_progress = QProgressBar()
|
||||
self.cpu_progress.setRange(0, 100)
|
||||
layout.addWidget(self.cpu_progress, 3, 1)
|
||||
|
||||
layout.addWidget(QLabel("GPU使用率:"), 4, 0)
|
||||
self.gpu_progress = QProgressBar()
|
||||
self.gpu_progress.setRange(0, 100)
|
||||
layout.addWidget(self.gpu_progress, 4, 1)
|
||||
|
||||
return group
|
||||
|
||||
def createControllerGroup(self):
|
||||
"""创建控制器状态组"""
|
||||
group = QGroupBox("控制器状态")
|
||||
layout = QVBoxLayout(group)
|
||||
|
||||
# 控制器列表
|
||||
self.controller_list = QTextEdit()
|
||||
self.controller_list.setMaximumHeight(100)
|
||||
self.controller_list.setReadOnly(True)
|
||||
layout.addWidget(self.controller_list)
|
||||
|
||||
# 触觉反馈测试
|
||||
haptic_layout = QHBoxLayout()
|
||||
haptic_layout.addWidget(QLabel("触觉反馈测试:"))
|
||||
|
||||
self.haptic_button = QPushButton("发送震动")
|
||||
self.haptic_button.clicked.connect(self.sendHapticFeedback)
|
||||
haptic_layout.addWidget(self.haptic_button)
|
||||
|
||||
layout.addLayout(haptic_layout)
|
||||
|
||||
return group
|
||||
|
||||
def createLogGroup(self):
|
||||
"""创建日志输出组"""
|
||||
group = QGroupBox("系统日志")
|
||||
layout = QVBoxLayout(group)
|
||||
|
||||
self.log_text = QTextEdit()
|
||||
self.log_text.setMaximumHeight(150)
|
||||
self.log_text.setReadOnly(True)
|
||||
layout.addWidget(self.log_text)
|
||||
|
||||
# 清空日志按钮
|
||||
clear_button = QPushButton("清空日志")
|
||||
clear_button.clicked.connect(self.clearLog)
|
||||
layout.addWidget(clear_button)
|
||||
|
||||
return group
|
||||
|
||||
def connectSignals(self):
|
||||
"""连接信号"""
|
||||
# 连接值变化信号
|
||||
self.width_spinbox.valueChanged.connect(self.onQualityChanged)
|
||||
self.height_spinbox.valueChanged.connect(self.onQualityChanged)
|
||||
self.fps_spinbox.valueChanged.connect(self.onQualityChanged)
|
||||
self.bitrate_spinbox.valueChanged.connect(self.onQualityChanged)
|
||||
|
||||
def enableVR(self):
|
||||
"""启用VR"""
|
||||
self.addLog("正在启用VR模式...")
|
||||
if self.world.enableVRMode():
|
||||
self.addLog("✓ VR模式已启用")
|
||||
self.vr_enabled.emit(True)
|
||||
else:
|
||||
self.addLog("✗ VR模式启用失败")
|
||||
|
||||
def disableVR(self):
|
||||
"""禁用VR"""
|
||||
self.addLog("正在禁用VR模式...")
|
||||
self.world.disableVRMode()
|
||||
self.addLog("✓ VR模式已禁用")
|
||||
self.vr_enabled.emit(False)
|
||||
|
||||
def startStreaming(self):
|
||||
"""开始串流"""
|
||||
self.addLog("正在开始ALVR串流...")
|
||||
if self.world.startALVRStreaming():
|
||||
self.addLog("✓ ALVR串流已开始")
|
||||
self.alvr_streaming_changed.emit(True)
|
||||
else:
|
||||
self.addLog("✗ ALVR串流开始失败")
|
||||
|
||||
def stopStreaming(self):
|
||||
"""停止串流"""
|
||||
self.addLog("正在停止ALVR串流...")
|
||||
self.world.stopALVRStreaming()
|
||||
self.addLog("✓ ALVR串流已停止")
|
||||
self.alvr_streaming_changed.emit(False)
|
||||
|
||||
def toggleControllerRays(self, checked):
|
||||
"""切换控制器射线显示"""
|
||||
self.world.showControllerRays(checked)
|
||||
self.addLog(f"控制器射线: {'显示' if checked else '隐藏'}")
|
||||
|
||||
def toggleGestureRecognition(self, checked):
|
||||
"""切换手势识别"""
|
||||
self.world.setVRGestureEnabled(checked)
|
||||
self.addLog(f"手势识别: {'启用' if checked else '禁用'}")
|
||||
|
||||
def toggleVRInteraction(self, checked):
|
||||
"""切换VR交互"""
|
||||
self.world.setVRInteractionEnabled(checked)
|
||||
self.addLog(f"VR交互: {'启用' if checked else '禁用'}")
|
||||
|
||||
def applyQualitySettings(self):
|
||||
"""应用质量设置"""
|
||||
width = self.width_spinbox.value()
|
||||
height = self.height_spinbox.value()
|
||||
fps = self.fps_spinbox.value()
|
||||
bitrate = self.bitrate_spinbox.value()
|
||||
|
||||
self.world.setALVRStreamQuality(width, height, fps, bitrate)
|
||||
self.addLog(f"串流质量已设置: {width}x{height} @ {fps}fps, {bitrate}Mbps")
|
||||
|
||||
def sendHapticFeedback(self):
|
||||
"""发送触觉反馈"""
|
||||
controllers = self.world.getAllControllers()
|
||||
if controllers:
|
||||
controller_id = controllers[0] # 使用第一个控制器
|
||||
self.world.sendHapticFeedback(controller_id, 0.5, 0.8)
|
||||
self.addLog(f"已发送触觉反馈到控制器 {controller_id}")
|
||||
else:
|
||||
self.addLog("没有可用的控制器")
|
||||
|
||||
def onQualityChanged(self):
|
||||
"""质量设置变化"""
|
||||
# 可以在这里实现实时质量调整
|
||||
pass
|
||||
|
||||
def updateStatus(self):
|
||||
"""更新状态显示"""
|
||||
# 更新VR状态
|
||||
vr_status = self.world.getVRStatus()
|
||||
|
||||
# VR系统状态
|
||||
if vr_status['vr_enabled']:
|
||||
self.vr_status_label.setText("VR状态: 已启用")
|
||||
self.vr_status_label.setStyleSheet("color: green; font-weight: bold;")
|
||||
self.enable_vr_button.setEnabled(False)
|
||||
self.disable_vr_button.setEnabled(True)
|
||||
|
||||
# 更新VR设备信息
|
||||
vr_info = vr_status['vr_info']
|
||||
if vr_info:
|
||||
device_info = f"VR设备: {vr_info.get('hmd_manufacturer', 'Unknown')} {vr_info.get('hmd_model', 'Unknown')}"
|
||||
self.vr_info_label.setText(device_info)
|
||||
else:
|
||||
self.vr_status_label.setText("VR状态: 未启用")
|
||||
self.vr_status_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
self.enable_vr_button.setEnabled(True)
|
||||
self.disable_vr_button.setEnabled(False)
|
||||
self.vr_info_label.setText("VR设备: 未连接")
|
||||
|
||||
# ALVR状态
|
||||
if vr_status['alvr_connected']:
|
||||
self.alvr_status_label.setText("ALVR状态: 已连接")
|
||||
self.alvr_status_label.setStyleSheet("color: green; font-weight: bold;")
|
||||
self.start_streaming_button.setEnabled(not vr_status['alvr_streaming'])
|
||||
self.stop_streaming_button.setEnabled(vr_status['alvr_streaming'])
|
||||
else:
|
||||
self.alvr_status_label.setText("ALVR状态: 未连接")
|
||||
self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
self.start_streaming_button.setEnabled(False)
|
||||
self.stop_streaming_button.setEnabled(False)
|
||||
|
||||
# 性能统计
|
||||
streaming_status = vr_status['streaming_status']
|
||||
if streaming_status:
|
||||
self.stream_fps_label.setText(str(streaming_status.get('fps', 0)))
|
||||
self.latency_label.setText(f"{streaming_status.get('latency', 0)} ms")
|
||||
|
||||
# 控制器状态
|
||||
controllers = vr_status['controllers']
|
||||
if controllers:
|
||||
controller_text = f"已连接 {len(controllers)} 个控制器:\n"
|
||||
for i, controller_id in enumerate(controllers):
|
||||
controller_text += f"控制器 {i+1}: ID {controller_id}\n"
|
||||
self.controller_list.setText(controller_text)
|
||||
else:
|
||||
self.controller_list.setText("没有连接的控制器")
|
||||
|
||||
def addLog(self, message):
|
||||
"""添加日志"""
|
||||
import datetime
|
||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
log_message = f"[{timestamp}] {message}"
|
||||
self.log_text.append(log_message)
|
||||
print(log_message) # 同时输出到控制台
|
||||
|
||||
def clearLog(self):
|
||||
"""清空日志"""
|
||||
self.log_text.clear()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭事件"""
|
||||
# 停止更新定时器
|
||||
if hasattr(self, 'update_timer'):
|
||||
self.update_timer.stop()
|
||||
|
||||
# 如果VR启用,先禁用
|
||||
if self.world.isVREnabled():
|
||||
self.disableVR()
|
||||
|
||||
event.accept()
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
468
vr_test.py
468
vr_test.py
@ -1,468 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
VR功能测试脚本
|
||||
测试VR系统的各种功能,包括模拟模式和真实VR模式
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from main import MyWorld
|
||||
|
||||
def test_basic_vr_functionality():
|
||||
"""测试基本VR功能"""
|
||||
print("=== VR基本功能测试 ===")
|
||||
|
||||
# 创建世界实例
|
||||
world = MyWorld()
|
||||
|
||||
# 检查VR管理器是否正确初始化
|
||||
print("✓ VR管理器已创建")
|
||||
|
||||
# 检查OpenVR库是否可用
|
||||
try:
|
||||
import openvr
|
||||
print("✓ OpenVR库已安装")
|
||||
except ImportError:
|
||||
print("⚠ OpenVR库未安装,将使用模拟模式")
|
||||
|
||||
# 测试VR初始化(会自动回退到模拟模式)
|
||||
print("\n正在测试VR初始化...")
|
||||
vr_success = world.initializeVR()
|
||||
|
||||
if vr_success:
|
||||
print("✓ VR系统初始化成功")
|
||||
|
||||
# 获取VR信息
|
||||
vr_info = world.getVRInfo()
|
||||
print(f"\n📊 VR系统信息:")
|
||||
print(f" - 模式: {vr_info['mode']}")
|
||||
print(f" - 启用状态: {vr_info['enabled']}")
|
||||
print(f" - 模拟模式: {vr_info['simulation_mode']}")
|
||||
print(f" - 渲染尺寸: {vr_info['render_size']}")
|
||||
print(f" - 控制器数量: {vr_info.get('controllers', 0)}")
|
||||
|
||||
# 测试控制器输入
|
||||
print("\n🎮 测试控制器输入:")
|
||||
for i in range(2):
|
||||
controller_input = world.vr_manager.get_controller_input(i)
|
||||
if controller_input and controller_input.get('connected'):
|
||||
print(f" 控制器 {i}: 已连接")
|
||||
print(f" - 扳机: {controller_input['trigger']:.2f}")
|
||||
print(f" - 握把: {controller_input['grip']:.2f}")
|
||||
print(f" - 触摸板: ({controller_input['touchpad']['x']:.2f}, {controller_input['touchpad']['y']:.2f})")
|
||||
else:
|
||||
print(f" 控制器 {i}: 未连接")
|
||||
|
||||
# 测试VR状态
|
||||
print("\n📊 VR系统状态:")
|
||||
status = world.getVRStatus()
|
||||
print(f" - VR启用: {status['vr_enabled']}")
|
||||
print(f" - 控制器总数: {len(status['controllers'])}")
|
||||
|
||||
else:
|
||||
print("✗ VR系统初始化失败")
|
||||
return False
|
||||
|
||||
# 关闭VR系统
|
||||
world.shutdownVR()
|
||||
print("\n✓ VR系统已关闭")
|
||||
|
||||
return True
|
||||
|
||||
def test_simulation_mode():
|
||||
"""测试VR模拟模式"""
|
||||
print("=== VR模拟模式测试 ===")
|
||||
|
||||
# 创建世界实例
|
||||
world = MyWorld()
|
||||
|
||||
# 强制启用模拟模式
|
||||
print("强制启用VR模拟模式...")
|
||||
vr_success = world.vr_manager.enable_simulation_mode()
|
||||
|
||||
if vr_success:
|
||||
print("✓ VR模拟模式启用成功")
|
||||
|
||||
# 获取模拟数据
|
||||
sim_data = world.vr_manager.get_simulation_data()
|
||||
if sim_data:
|
||||
print(f"\n🎮 模拟数据:")
|
||||
print(f" - 头部位置: {sim_data['head_pose']['position']}")
|
||||
print(f" - 控制器 0 位置: {sim_data['controller_poses'][0]['position']}")
|
||||
print(f" - 控制器 1 位置: {sim_data['controller_poses'][1]['position']}")
|
||||
print(f" - 渲染尺寸: {sim_data['render_size']}")
|
||||
|
||||
# 测试更新模拟数据
|
||||
print(f"\n🔧 测试模拟数据更新:")
|
||||
new_head_pos = [0.1, 0.1, 1.7]
|
||||
success = world.vr_manager.update_simulation_data('head_pose', {
|
||||
'position': new_head_pos,
|
||||
'rotation': [0, 0, 0, 1]
|
||||
})
|
||||
if success:
|
||||
print(f" ✓ 头部位置更新为: {new_head_pos}")
|
||||
|
||||
# 测试控制器输入(模拟)
|
||||
print(f"\n🎮 模拟控制器输入测试:")
|
||||
for i in range(2):
|
||||
controller_input = world.vr_manager.get_controller_input(i)
|
||||
if controller_input:
|
||||
print(f" 控制器 {i}:")
|
||||
print(f" - 连接状态: {controller_input['connected']}")
|
||||
print(f" - 扳机: {controller_input['trigger']}")
|
||||
print(f" - 握把: {controller_input['grip']}")
|
||||
print(f" - 菜单键: {controller_input['menu']}")
|
||||
|
||||
else:
|
||||
print("✗ VR模拟模式启用失败")
|
||||
return False
|
||||
|
||||
# 关闭VR系统
|
||||
world.vr_manager.shutdown_vr()
|
||||
print("\n✓ VR模拟模式已关闭")
|
||||
|
||||
return True
|
||||
|
||||
def test_alvr_streaming():
|
||||
"""测试ALVR串流功能"""
|
||||
print("=== ALVR串流测试 ===")
|
||||
|
||||
# 创建世界实例
|
||||
world = MyWorld()
|
||||
|
||||
# 初始化VR系统
|
||||
print("初始化VR系统...")
|
||||
if not world.initializeVR():
|
||||
print("✗ VR系统初始化失败")
|
||||
return False
|
||||
|
||||
# 测试ALVR初始化
|
||||
print("\n正在测试ALVR初始化...")
|
||||
alvr_success = world.initializeALVR()
|
||||
|
||||
if alvr_success:
|
||||
print("✓ ALVR初始化成功")
|
||||
|
||||
# 测试串流状态
|
||||
print("\n📊 ALVR串流状态:")
|
||||
status = world.getALVRStreamingStatus()
|
||||
print(f" - 连接状态: {world.isALVRConnected()}")
|
||||
print(f" - 串流状态: {world.isALVRStreaming()}")
|
||||
|
||||
# 测试串流质量设置
|
||||
print("\n🎥 测试串流质量设置:")
|
||||
quality_success = world.setALVRStreamQuality(1920, 1080, 60, 100)
|
||||
if quality_success:
|
||||
print(" ✓ 串流质量设置成功: 1920x1080@60fps, 100Mbps")
|
||||
else:
|
||||
print(" ⚠ 串流质量设置失败")
|
||||
|
||||
# 测试触觉反馈
|
||||
print("\n🔔 测试触觉反馈:")
|
||||
for controller_id in range(2):
|
||||
feedback_success = world.sendHapticFeedback(controller_id, 0.1, 0.5)
|
||||
if feedback_success:
|
||||
print(f" ✓ 控制器 {controller_id} 触觉反馈发送成功")
|
||||
else:
|
||||
print(f" ⚠ 控制器 {controller_id} 触觉反馈发送失败")
|
||||
|
||||
# 测试串流控制
|
||||
print("\n🎮 测试串流控制:")
|
||||
start_success = world.startALVRStreaming()
|
||||
if start_success:
|
||||
print(" ✓ 串流开始成功")
|
||||
else:
|
||||
print(" ⚠ 串流开始失败")
|
||||
|
||||
# 等待一下然后停止
|
||||
import time
|
||||
time.sleep(1)
|
||||
|
||||
stop_success = world.stopALVRStreaming()
|
||||
if stop_success:
|
||||
print(" ✓ 串流停止成功")
|
||||
else:
|
||||
print(" ⚠ 串流停止失败")
|
||||
|
||||
else:
|
||||
print("⚠ ALVR初始化失败(这在没有ALVR服务器时是正常的)")
|
||||
print(" 提示: 要使用ALVR功能,请:")
|
||||
print(" 1. 下载并安装ALVR服务器")
|
||||
print(" 2. 启动ALVR服务器")
|
||||
print(" 3. 在Quest头盔上安装并启动ALVR客户端")
|
||||
|
||||
# 关闭系统
|
||||
world.shutdownALVR()
|
||||
world.shutdownVR()
|
||||
print("\n✓ ALVR和VR系统已关闭")
|
||||
|
||||
return True
|
||||
|
||||
def test_vr_gui_control_panel():
|
||||
"""测试VR GUI控制面板"""
|
||||
print("=== VR GUI控制面板测试 ===")
|
||||
|
||||
try:
|
||||
from ui.vr_control_panel import VRControlPanel
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
# 创建Qt应用程序
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 创建世界实例
|
||||
world = MyWorld()
|
||||
|
||||
# 创建VR控制面板
|
||||
print("创建VR控制面板...")
|
||||
control_panel = VRControlPanel(world)
|
||||
|
||||
# 测试面板功能
|
||||
print("✓ VR控制面板创建成功")
|
||||
print(" - 面板标题:", control_panel.windowTitle())
|
||||
print(" - 面板大小:", control_panel.size().width(), "x", control_panel.size().height())
|
||||
|
||||
# 显示面板(非阻塞)
|
||||
control_panel.show()
|
||||
print("✓ VR控制面板已显示")
|
||||
|
||||
print("\n💡 GUI控制面板功能:")
|
||||
print(" - VR系统开关控制")
|
||||
print(" - ALVR串流控制")
|
||||
print(" - 实时状态监控")
|
||||
print(" - 质量设置调整")
|
||||
print(" - 控制器状态显示")
|
||||
print(" - 性能监控")
|
||||
|
||||
# 短暂显示然后关闭
|
||||
import time
|
||||
time.sleep(2)
|
||||
control_panel.close()
|
||||
|
||||
print("\n✓ VR控制面板测试完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ VR控制面板测试失败: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_vr_interaction():
|
||||
"""测试VR交互功能"""
|
||||
print("=== VR交互功能测试 ===")
|
||||
|
||||
# 创建世界实例
|
||||
world = MyWorld()
|
||||
|
||||
# 初始化VR系统
|
||||
print("初始化VR系统...")
|
||||
if not world.initializeVR():
|
||||
print("✗ VR系统初始化失败")
|
||||
return False
|
||||
|
||||
# 测试VR输入处理
|
||||
print("\n🎮 测试VR输入处理:")
|
||||
input_success = world.startVRInput()
|
||||
if input_success:
|
||||
print(" ✓ VR输入处理启动成功")
|
||||
|
||||
# 测试控制器射线显示
|
||||
print("\n🌟 测试控制器射线:")
|
||||
ray_success = world.showControllerRays(True)
|
||||
if ray_success:
|
||||
print(" ✓ 控制器射线显示启用")
|
||||
else:
|
||||
print(" ⚠ 控制器射线显示失败")
|
||||
|
||||
# 测试手势识别
|
||||
print("\n✋ 测试手势识别:")
|
||||
gesture_success = world.setVRGestureEnabled(True)
|
||||
if gesture_success:
|
||||
print(" ✓ VR手势识别启用")
|
||||
else:
|
||||
print(" ⚠ VR手势识别启用失败")
|
||||
|
||||
# 测试VR交互
|
||||
print("\n🤏 测试VR交互:")
|
||||
interaction_success = world.setVRInteractionEnabled(True)
|
||||
if interaction_success:
|
||||
print(" ✓ VR交互功能启用")
|
||||
else:
|
||||
print(" ⚠ VR交互功能启用失败")
|
||||
|
||||
# 获取控制器状态
|
||||
print("\n🎮 控制器状态:")
|
||||
controllers = world.getAllControllers()
|
||||
for i, controller in enumerate(controllers):
|
||||
if controller:
|
||||
print(f" 控制器 {i}: 可用")
|
||||
state = world.getControllerState(i)
|
||||
if state:
|
||||
print(f" - 位置: {state.get('position', 'N/A')}")
|
||||
print(f" - 旋转: {state.get('rotation', 'N/A')}")
|
||||
print(f" - 按钮: {state.get('buttons', 'N/A')}")
|
||||
else:
|
||||
print(f" 控制器 {i}: 不可用")
|
||||
|
||||
# 停止VR输入
|
||||
world.stopVRInput()
|
||||
print("\n✓ VR输入处理已停止")
|
||||
|
||||
else:
|
||||
print(" ⚠ VR输入处理启动失败")
|
||||
|
||||
# 关闭VR系统
|
||||
world.shutdownVR()
|
||||
print("\n✓ VR系统已关闭")
|
||||
|
||||
return True
|
||||
|
||||
def run_all_tests():
|
||||
"""运行所有测试"""
|
||||
print("=== 运行所有VR测试 ===")
|
||||
|
||||
tests = [
|
||||
("基本VR功能", test_basic_vr_functionality),
|
||||
("VR模拟模式", test_simulation_mode),
|
||||
("ALVR串流", test_alvr_streaming),
|
||||
("VR GUI控制面板", test_vr_gui_control_panel),
|
||||
("VR交互功能", test_vr_interaction)
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
print(f"\n{'='*50}")
|
||||
print(f"正在运行: {test_name}")
|
||||
print('='*50)
|
||||
|
||||
try:
|
||||
success = test_func()
|
||||
results.append((test_name, success))
|
||||
if success:
|
||||
print(f"✓ {test_name} 测试通过")
|
||||
else:
|
||||
print(f"✗ {test_name} 测试失败")
|
||||
except Exception as e:
|
||||
print(f"✗ {test_name} 测试出错: {str(e)}")
|
||||
results.append((test_name, False))
|
||||
|
||||
# 打印总结
|
||||
print(f"\n{'='*50}")
|
||||
print("测试总结")
|
||||
print('='*50)
|
||||
|
||||
passed = sum(1 for _, success in results if success)
|
||||
total = len(results)
|
||||
|
||||
for test_name, success in results:
|
||||
status = "✓ 通过" if success else "✗ 失败"
|
||||
print(f"{test_name}: {status}")
|
||||
|
||||
print(f"\n总计: {passed}/{total} 个测试通过")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 所有测试都通过了!")
|
||||
else:
|
||||
print("⚠ 部分测试失败,请检查上述输出")
|
||||
|
||||
return passed == total
|
||||
|
||||
def print_vr_requirements():
|
||||
"""打印VR系统要求"""
|
||||
print("📋 VR系统要求说明")
|
||||
print("="*50)
|
||||
print("🔧 软件要求:")
|
||||
print(" - Python 3.8+")
|
||||
print(" - Panda3D")
|
||||
print(" - PyQt5")
|
||||
print(" - OpenVR库 (pip install openvr)")
|
||||
print(" - 其他依赖见 requirements/vr-requirements.txt")
|
||||
|
||||
print("\n🎮 硬件要求(完整VR模式):")
|
||||
print(" - 支持VR的显卡 (GTX 1060/RX 580 或更好)")
|
||||
print(" - VR头盔 (Quest 2/3, Valve Index, HTC Vive等)")
|
||||
print(" - 足够的USB端口或无线连接")
|
||||
|
||||
print("\n🌐 连接方式:")
|
||||
print(" 有线连接:")
|
||||
print(" - Valve Index: DisplayPort + USB 3.0")
|
||||
print(" - HTC Vive: HDMI + USB 3.0")
|
||||
print(" - Quest 2/3: USB-C (Quest Link)")
|
||||
print(" 无线连接:")
|
||||
print(" - Quest 2/3: 通过ALVR串流")
|
||||
print(" - 需要5GHz Wi-Fi网络")
|
||||
print(" - 需要ALVR服务器运行")
|
||||
|
||||
print("\n🔄 启动顺序(完整VR模式):")
|
||||
print(" 1. 确保VR头盔已连接并识别")
|
||||
print(" 2. 启动SteamVR")
|
||||
print(" 3. (可选) 启动ALVR服务器(Quest无线)")
|
||||
print(" 4. 运行VR测试脚本")
|
||||
|
||||
print("\n🎮 模拟模式说明:")
|
||||
print(" - 无需VR硬件")
|
||||
print(" - 模拟头盔和控制器追踪")
|
||||
print(" - 立体渲染到窗口")
|
||||
print(" - 适用于开发和调试")
|
||||
|
||||
print("\n💡 故障排除:")
|
||||
print(" - 如果OpenVR初始化失败,系统会自动切换到模拟模式")
|
||||
print(" - 检查SteamVR是否正在运行")
|
||||
print(" - 确认VR头盔被系统识别")
|
||||
print(" - 检查USB/DisplayPort连接")
|
||||
print(" - 重启SteamVR和头盔")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🎮 VR功能测试脚本")
|
||||
print("="*50)
|
||||
|
||||
# 显示要求说明
|
||||
print_vr_requirements()
|
||||
|
||||
print("\n请选择测试类型:")
|
||||
print("1. 基本VR功能测试")
|
||||
print("2. VR模拟模式测试")
|
||||
print("3. ALVR串流测试")
|
||||
print("4. VR GUI控制面板")
|
||||
print("5. VR交互功能测试")
|
||||
print("6. 运行所有测试")
|
||||
print("7. 查看VR系统要求")
|
||||
|
||||
try:
|
||||
choice = input("请输入选择 (1-7): ")
|
||||
|
||||
if choice == "1":
|
||||
success = test_basic_vr_functionality()
|
||||
elif choice == "2":
|
||||
success = test_simulation_mode()
|
||||
elif choice == "3":
|
||||
success = test_alvr_streaming()
|
||||
elif choice == "4":
|
||||
success = test_vr_gui_control_panel()
|
||||
elif choice == "5":
|
||||
success = test_vr_interaction()
|
||||
elif choice == "6":
|
||||
success = run_all_tests()
|
||||
elif choice == "7":
|
||||
print_vr_requirements()
|
||||
return
|
||||
else:
|
||||
print("无效的选择")
|
||||
return
|
||||
|
||||
print("\n" + "="*50)
|
||||
if success:
|
||||
print("✓ 测试成功")
|
||||
else:
|
||||
print("✗ 测试失败")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断测试")
|
||||
except Exception as e:
|
||||
print(f"测试过程中发生错误: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user