Merge pull request 'VR' (#3) from VR into main

Reviewed-on: #3
This commit is contained in:
Rowland 2025-09-15 08:44:03 +00:00
commit 5481082588
26 changed files with 3670 additions and 3121 deletions

112
CLAUDE.md Normal file
View 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支持
- 某些功能可能需要特定的系统配置

View File

@ -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")

View File

@ -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)

View File

@ -1,6 +0,0 @@
# Autogenerated
name = 'Plastic-R0.0'
roughness = 0.0
ior = 1.51
basecolor = (1, 0, 0)
mat_type = 'default'

View File

@ -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
View 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)对于vector2float对于vector1
"""
if not self.vr_input or action_name not in self.action_handles:
return None, None
try:
action_handle = self.action_handles[action_name]
device_handle = openvr.k_ulInvalidInputValueHandle
if device_path:
device_handle = self.vr_input.getInputSourceHandle(device_path)
analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle)
if device_path and analog_data.bActive:
origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin)
device_path = origin_info.devicePath
if analog_data.bActive:
# 根据动作类型返回适当的值
from panda3d.core import Vec2
if action_name in ['trackpad', 'joystick']:
return Vec2(analog_data.x, analog_data.y), device_path
else:
return analog_data.x, device_path
return None, device_path
except Exception as e:
return None, None
def get_pose_action_data(self, action_name, device_path=None):
"""获取姿态动作数据"""
if not self.vr_input or action_name not in self.action_handles:
return None
try:
action_handle = self.action_handles[action_name]
device_handle = openvr.k_ulInvalidInputValueHandle
if device_path:
device_handle = self.vr_input.getInputSourceHandle(device_path)
pose_data = self.vr_input.getPoseActionDataForNextFrame(
action_handle,
openvr.TrackingUniverseStanding,
device_handle
)
return pose_data
except Exception as e:
return None
def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None):
"""触发震动反馈
Args:
action_name: 震动动作名称
duration: 持续时间
frequency: 频率
amplitude: 振幅 (0.0-1.0)
device_path: 设备路径可选
"""
if not self.vr_input or action_name not in self.action_handles:
return False
try:
action_handle = self.action_handles[action_name]
device_handle = openvr.k_ulInvalidInputValueHandle
if device_path:
device_handle = self.vr_input.getInputSourceHandle(device_path)
# 触发震动
self.vr_input.triggerHapticVibrationAction(
action_handle,
0, # 开始时间
duration,
frequency,
amplitude,
device_handle
)
return True
except Exception as e:
print(f"⚠️ 触发震动反馈失败: {e}")
return False
def cleanup(self):
"""清理资源"""
self.ignoreAll()
# 清理动作句柄
self.action_handles.clear()
self.action_set_handles.clear()
print("🧹 VR动作管理器已清理")

282
core/vr_controller.py Normal file
View 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')

View File

@ -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
View 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交互管理器已清理")

File diff suppressed because it is too large Load Diff

658
core/vr_visualization.py Normal file
View 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}手柄可视化已清理")

View File

@ -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文件。

View File

@ -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
View File

@ -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)

View File

@ -98,3 +98,4 @@ webencodings==0.5.1
xdg==5
xkit==0.0.0
zipp==1.0.0
openvr==2.2.0

View File

@ -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

View File

@ -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()

View File

@ -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
View 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": "震动反馈"
}
]
}

View 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"
}
]
}
}
}

View File

@ -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()