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

658 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
VR可视化模块
提供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}手柄可视化已清理")