1
0
forked from Rowland/EG

8.28合并

This commit is contained in:
Hector 2025-09-01 09:06:16 +08:00
parent 95395bdb05
commit d980330d9d
11 changed files with 519 additions and 175 deletions

View File

@ -53,7 +53,7 @@ class Panda3DWorld(ShowBase):
Panda3DWorld : A class to handle all panda3D world manipulation
"""
def __init__(self, width=1380, height=729, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
name="qpanda3D"):
global _global_world_instance

View File

@ -114,7 +114,7 @@ class EventHandler:
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件"""
print("\n=== 开始处理鼠标左键事件 ===")
print(f"当前工具: {self.world.currentTool}")
#print(f"当前工具: {self.world.currentTool}")
if not evt:
print("事件为空")
@ -126,7 +126,7 @@ class EventHandler:
# 获取鼠标点击的位置
x = evt.get('x', 0)
y = evt.get('y', 0)
print(f"鼠标点击位置: ({x}, {y})")
#print(f"鼠标点击位置: ({x}, {y})")
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
@ -134,20 +134,20 @@ class EventHandler:
# 直接使用 x, y 创建鼠标位置
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
print(f"转换后的坐标: ({mx}, {my})")
#print(f"转换后的坐标: ({mx}, {my})")
# 创建射线
nearPoint = Point3()
farPoint = Point3()
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
print(f"相机坐标系射线起点: {nearPoint}")
print(f"相机坐标系射线终点: {farPoint}")
#print(f"相机坐标系射线起点: {nearPoint}")
#print(f"相机坐标系射线终点: {farPoint}")
# 将相机坐标系的点转换到世界坐标系
worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint)
worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint)
print(f"世界坐标系射线起点: {worldNearPoint}")
print(f"世界坐标系射线终点: {worldFarPoint}")
#print(f"世界坐标系射线起点: {worldNearPoint}")
#print(f"世界坐标系射线终点: {worldFarPoint}")
# 进行射线检测
picker = CollisionTraverser()

View File

@ -63,7 +63,49 @@ class SelectionSystem:
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
}
self._current_cursor = None
self._default_cursor = None
print("✓ 选择和变换系统初始化完成")
# ==================== 光标设置 ====================
def _setCursor(self,cursor_type):
try:
from PyQt5.QtCore import Qt
if self._current_cursor == cursor_type:
return
if hasattr(self.world,'main_window') and self.world.main_window:
main_window = self.world.main_window
else:
from PyQt5.QtWidgets import QApplication
main_window = QApplication.activeWindow()
if not main_window:
windows = QApplication.topLevelWindows()
for window in windows:
if hasattr(window,'isVisible') and window.isVisible():
main_window = window
break
if main_window:
if cursor_type == "crosshair":
main_window.setCursor(Qt.CrossCursor)
elif cursor_type == "size_hor":
main_window.setCursor(Qt.SizeHorCursor)
elif cursor_type == "size_ver":
main_window.setCursor(Qt.SizeVerCursor)
elif cursor_type == "size_all":
main_window.setCursor(Qt.SizeAllCursor)
elif cursor_type == "pointing_hand":
main_window.setCursor(Qt.PointingHandCursor)
else:
main_window.unsetCursor()
self._current_cursor = cursor_type
print(f"光标已设置:{cursor_type}")
self._current_cursor = cursor_type
else:
print("警告:无法获取主窗口,光标设置失败")
except Exception as e:
print(f"设置光标失败{e}")
def _resetCursor(self):
self._setCursor("default")
# ==================== 选择框系统 ====================
@ -108,6 +150,26 @@ class SelectionSystem:
if not self.selectionBox or not self.selectionBoxTarget:
return
if self.selectionBoxTarget.isEmpty():
return
minPoint = Point3()
maxPoint = Point3()
try:
has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render)
if not has_bounds:
return
except:
return
# 检查边界框的有效性
if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or
abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or
abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10):
print("警告: 检测到无效的边界框,跳过选择框更新")
return
# 检查是否需要重新计算边界框
if not hasattr(self, '_bounds_cache'):
self._bounds_cache = {}
@ -471,8 +533,8 @@ class SelectionSystem:
self.gizmo.setShaderOff() # 禁用着色器
self.gizmo.setFogOff() # 禁用雾效
self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级数值越大越优先
self.gizmo.setDepthWrite(False) # 禁用深度写入
self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见
#self.gizmo.setDepthWrite(False) # 禁用深度写入
#self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见
# 设置各轴节点的渲染属性
for axis_node in axis_nodes:
@ -481,8 +543,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40) # 与主节点相同优先级
axis_node.setDepthWrite(False) # 禁用深度写入
axis_node.setDepthTest(False) # 禁用深度测试
#axis_node.setDepthWrite(False) # 禁用深度写入
#axis_node.setDepthTest(False) # 禁用深度测试
# 设置旋转轴节点的渲染属性
for axis_rotnode in axis_Rotnodes:
@ -491,8 +553,8 @@ class SelectionSystem:
axis_rotnode.setShaderOff()
axis_rotnode.setFogOff()
axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级
axis_rotnode.setDepthWrite(False) # 禁用深度写入
axis_rotnode.setDepthTest(False) # 禁用深度测试
#axis_rotnode.setDepthWrite(False) # 禁用深度写入
#axis_rotnode.setDepthTest(False) # 禁用深度测试
# 收集所有handle节点
arrow_nodes = []
@ -530,8 +592,8 @@ class SelectionSystem:
arrow_node.setShaderOff()
arrow_node.setFogOff()
arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
arrow_node.setDepthWrite(False)
arrow_node.setDepthTest(False)
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(False)
# 启用透明度支持
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
@ -541,8 +603,8 @@ class SelectionSystem:
rot_node.setShaderOff()
rot_node.setFogOff()
rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
rot_node.setDepthWrite(False)
rot_node.setDepthTest(False)
#rot_node.setDepthWrite(False)
#rot_node.setDepthTest(False)
# 启用透明度支持
rot_node.setTransparency(TransparencyAttrib.MAlpha)
@ -640,12 +702,25 @@ class SelectionSystem:
if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框
minPoint = Point3()
maxPoint = Point3()
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# 计算中心点
center = Point3((minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5)
self.gizmo.setPos(center)
# 添加异常处理
try:
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# 检查边界框的有效性
if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and
abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10):
# 计算中心点
center = Point3((minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5)
self.gizmo.setPos(center)
except Exception as e:
print(f"更新Gizmo位置时出错: {e}")
# if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# # 计算中心点
# center = Point3((minPoint.x + maxPoint.x) * 0.5,
# (minPoint.y + maxPoint.y) * 0.5,
# (minPoint.z + maxPoint.z) * 0.5)
# self.gizmo.setPos(center)
self._last_gizmo_bounds_update = current_time
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
@ -733,6 +808,7 @@ class SelectionSystem:
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
self.gizmoStartPos = None
self._resetCursor()
# def setGizmoAxisColor(self, axis, color):
@ -839,8 +915,8 @@ class SelectionSystem:
handle_node.setFogOff() # 禁用雾效果
handle_node.setBin("fixed",41)
handle_node.setDepthWrite(False)
handle_node.setDepthTest(False)
#handle_node.setDepthWrite(False)
#handle_node.setDepthTest(False)
# 保存材质引用以便后续修改
if axis == "x":
@ -854,8 +930,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(False)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(False)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
@ -889,15 +965,18 @@ class SelectionSystem:
axis_node = axis_nodes[axis]
if axis_node.isEmpty():
return
handle_node = None
handle_node = axis_node.find("x_handle") if axis == "x" else handle_node
handle_node = axis_node.find("y_handle") if axis == "y" else handle_node
handle_node = axis_node.find("z_handle") if axis == "z" else handle_node
#如果找不到特定名称的节点,尝试查找任何子节点
if not handle_node:
# 如果找不到特定名称的节点,尝试查找任何子节点
if not handle_node or handle_node.isEmpty():
children = axis_node.getChildren()
if children.getNumPath()>0:
if children.getNumPaths() > 0:
handle_node = children[0]
if not handle_node:
@ -907,15 +986,6 @@ class SelectionSystem:
# 创建或获取材质
mat = Material()
# # 设置材质属性 - 使用自发光确保在RenderPipeline下可见
# mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
# mat.setDiffuse(Vec4(0, 0, 0, 1))
# #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
# mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
# mat.set_roughness(1)
# 设置材质属性 - 使用更自然的颜色,避免过亮的自发光
# 将颜色值控制在合理范围内
adjusted_color = Vec4(
min(color[0], 1.0),
min(color[1], 1.0),
@ -924,16 +994,11 @@ class SelectionSystem:
)
mat.setBaseColor(adjusted_color)
#mat.setDiffuse(adjusted_color * 1) # 稍微降低漫反射亮度
#mat.setAmbient(adjusted_color * 0.4) # 设置环境光反射
# mat.setSpecular(Vec4(0.4, 0.4, 0.4, 1.0)) # 适度的镜面反射
# mat.setShininess(1.0) # 适中的高光强度
mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
# 应用材质
handle_node.setMaterial(mat, 1)
# 设置透明度
if color[3] < 1.0:
handle_node.setTransparency(TransparencyAttrib.MAlpha)
@ -945,8 +1010,8 @@ class SelectionSystem:
handle_node.setFogOff() # 禁用雾效果
handle_node.setBin("fixed",41)
handle_node.setDepthWrite(False)
handle_node.setDepthTest(False)
#handle_node.setDepthWrite(False)
#handle_node.setDepthTest(False)
# 保存材质引用以便后续修改
if axis == "x":
@ -960,8 +1025,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(False)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(False)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
@ -1336,6 +1401,7 @@ class SelectionSystem:
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
if not self.gizmo or self.isDraggingGizmo:
self._resetCursor()
return
# 使用碰撞检测方法
@ -1357,15 +1423,19 @@ class SelectionSystem:
# 高亮新的轴
if hoveredAxis:
self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis])
self._setCursor("pointing_hand")
else:
# 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色
for axis_name in ["x", "y", "z"]:
if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self._resetCursor()
self.gizmoHighlightAxis = hoveredAxis
self._last_detected_axis = hoveredAxis
elif hoveredAxis is None:
self._resetCursor()
def _detectHoveredAxis(self, mouseX, mouseY):
"""检测鼠标悬停的轴 - 提取为独立方法"""
@ -1445,6 +1515,13 @@ class SelectionSystem:
# self.dragGizmoAxis = axis
#
# self.gizmoHighlightAxis = self.dragGizmoAxis
# 设置拖拽光标
if self.dragGizmoAxis == "x":
self._setCursor("size_all") # 水平调整光标
elif self.dragGizmoAxis == "y":
self._setCursor("size_all") # 垂直调整光标
elif self.dragGizmoAxis == "z":
self._setCursor("size_all") # 全向调整光标
print(
f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
@ -1487,6 +1564,9 @@ class SelectionSystem:
if is_scale_tool:
scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01
scale_factor = max(0.001, scale_factor)
start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1))
if is_gui_element:
@ -1514,6 +1594,12 @@ class SelectionSystem:
start_scale.y * scale_factor,
start_scale.z * scale_factor)
new_scale = Vec3(
max(0.001,new_scale.x),
max(0.001,new_scale.y),
max(0.001,new_scale.z)
)
# 应用新缩放值
self.gizmoTarget.setScale(new_scale)
# 安全地更新属性面板
@ -1776,6 +1862,7 @@ class SelectionSystem:
# 重置高亮轴
self.gizmoHighlightAxis = None
self._resetCursor()
# ==================== 选择管理 ====================
def updateSelection(self, nodePath):

View File

@ -390,9 +390,6 @@ class TerrainManager:
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
print(f"✓ 重新设置了地形碰撞体")
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
return modified

View File

@ -399,14 +399,14 @@ class CoreWorld(Panda3DWorld):
def mousePressEventRight(self, evt):
"""处理鼠标右键按下事件"""
print("右键按下")
#print("右键按下")
self.mouseRightPressed = True
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
print("右键释放")
#print("右键释放")
self.mouseRightPressed = False
def mouseMoveEvent(self, evt):

View File

@ -24,6 +24,78 @@ except ImportError:
WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1])
else:
x_size = y_size = float(size[0]) if size else 1.0
else:
x_size = y_size = float(size)
# 创建卡片
cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1)
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
self.update3DImageTexture(image_node, image_path)
# 应用PBR效果如果可用
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
image_node,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": False,
"render_envmap": True,
"disable_children_effects": True
},
50
)
print("✓ GUI 3D图像PBR效果已应用")
except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
# 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1")
self.gui_elements.append(image_node)
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node
class GUIManager:
"""GUI元素管理系统类"""
@ -1887,28 +1959,9 @@ class GUIManager:
if color.isValid():
r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0
self.editGUIElement(gui_element, "color", [r, g, b, 1.0])
# def editGUI2DPosition(self, gui_element, axis, value):
# """编辑2D GUI元素位置"""
# try:
# current_pos = gui_element.getPos()
#
# if axis == "x":
# # 将逻辑坐标转换为屏幕坐标
# new_screen_x = value * 0.1
# gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ())
# elif axis == "z":
# # 将逻辑坐标转换为屏幕坐标
# new_screen_z = value * 0.1
# gui_element.setPos(current_pos.getX(), current_pos.getY(), new_screen_z)
#
# print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})")
#
# except Exception as e:
# print(f"编辑2D GUI位置失败: {str(e)}")
def update3DImageTexture(self, model_nodepath, image_path):
from panda3d.core import Texture
from panda3d.core import Texture, TextureStage
try:
# 加载新纹理
@ -1918,14 +1971,21 @@ class GUIManager:
new_texture.setMagfilter(Texture.FT_linear)
new_texture.setMinfilter(Texture.FT_linear_mipmap_linear)
# 应用纹理到模型
model_nodepath.setTexture(new_texture, 1)
model_nodepath.clearTexture()
# 为3D图像创建独立的纹理阶段
image_stage = TextureStage("3d_image_texture")
image_stage.setSort(0) # 使用第一个纹理槽
image_stage.setMode(TextureStage.MModulate) # 使用调制模式
# 应用纹理到模型,使用独立的纹理阶段
model_nodepath.setTexture(image_stage, new_texture)
# 更新标签
model_nodepath.setTag("gui_image_path", image_path)
# 确保材质设置正确
if not model_nodepath.has_material():
if not model_nodepath.hasMaterial():
from panda3d.core import Material, LColor
mat = Material()
mat.setName(f"image-material-{id(model_nodepath)}")
@ -1936,11 +1996,72 @@ class GUIManager:
mat.setShininess(10.0)
model_nodepath.setMaterial(mat, 1)
# 保护子节点的纹理特别是3D文本
self._preserveChildNodeTextures(model_nodepath)
print(f"✅ 3D图像纹理已更新为: {image_path}")
return True
else:
print(f"❌ 无法加载纹理: {image_path}")
return False
except Exception as e:
print(f"❌ 更新纹理时出错: {e}")
return False
def _preserveChildNodeTextures(self, parent_node):
"""保护子节点的纹理不被父节点纹理影响"""
try:
# 遍历所有直接子节点
for i in range(parent_node.getNumChildren()):
child = parent_node.getChild(i)
# 检查子节点是否为3D文本或其他需要特殊处理的节点
if self._is3DTextElement(child):
# 为3D文本创建独立的纹理阶段
self._restore3DTextTexture(child)
elif self._isOtherSpecialElement(child):
# 为其他特殊元素恢复纹理
self._restoreSpecialElementTexture(child)
except Exception as e:
print(f"保护子节点纹理时出错: {e}")
def _is3DTextElement(self, node):
"""检查节点是否为3D文本元素"""
try:
return (hasattr(node, 'getTag') and
node.getTag("gui_type") == "3d_text")
except:
return False
def _isOtherSpecialElement(self, node):
"""检查节点是否为其他需要特殊处理的元素"""
try:
return (hasattr(node, 'getTag') and
node.getTag("gui_type") in ["3d_image", "button", "label"])
except:
return False
def _restore3DTextTexture(self, text_node):
"""恢复3D文本的纹理设置"""
try:
from panda3d.core import TextureStage
# 如果3D文本已经有字体纹理确保它使用正确的纹理阶段
if hasattr(text_node, 'getTexture') and text_node.getTexture():
# 创建专门用于文本的纹理阶段
text_stage = TextureStage("text_texture")
text_stage.setSort(10) # 使用较高的纹理槽索引
text_stage.setMode(TextureStage.MModulate)
# 重新应用文本纹理
current_texture = text_node.getTexture()
text_node.setTexture(text_stage, current_texture)
print(f"已恢复3D文本纹理: {text_node.getName()}")
except Exception as e:
print(f"恢复3D文本纹理失败: {e}")
def update2DImageTexture(self, gui_element, image_path):
"""更新2D图片纹理"""

View File

@ -87,7 +87,7 @@ class SceneManager:
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""导入模型到场景 - 只在根节点下创建
@ -98,15 +98,15 @@ class SceneManager:
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
@ -154,14 +154,27 @@ class SceneManager:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# # 应用处理选项
# if apply_unit_conversion and filepath.lower().endswith('.fbx'):
# print("应用FBX单位转换厘米到米...")
# self._applyUnitConversion(model, 0.01)
# model.setTag("unit_conversion_applied", "true")
#
# if normalize_scales and filepath.lower().endswith('.fbx'):
# print("标准化FBX模型缩放层级...")
# self._normalizeModelScales(model)
# model.setTag("scale_normalization_applied", "true")
# 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
# 对于GLB文件通常不需要单位转换因为它们已经是标准单位
if apply_unit_conversion and filepath.lower().endswith(
('.fbx', '.obj')) and not filepath.lower().endswith('.glb'):
print("应用单位转换(厘米到米)...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
if normalize_scales and filepath.lower().endswith(('.fbx', '.obj')):
print("标准化模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
@ -429,6 +442,8 @@ class SceneManager:
print("模型已应用过单位转换,跳过")
return
# 获取当前边界用于后续位置调整
original_bounds = model.getBounds()
@ -734,7 +749,7 @@ class SceneManager:
# 根据调试设置决定是否显示碰撞体
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.show()
cNodePath.hide()
else:
cNodePath.hide()
@ -1053,9 +1068,7 @@ class SceneManager:
light.casts_shadows = True
light.shadow_map_resolution = 256
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.setPos(*pos)
# 添加到渲染管线
render_pipeline.add_light(light)
@ -1152,9 +1165,7 @@ class SceneManager:
# 创建点光源对象
light = PointLight()
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.setPos(*pos)
light.energy = 5000
light.radius = 1000

View File

@ -1,3 +1,4 @@
from PIL.ImageChops import lighter
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle
from PyQt5.QtCore import Qt
from PyQt5.sip import delete
@ -79,19 +80,113 @@ class InterfaceManager:
deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
else:
# 为模型节点或其子节点添加删除选项
parentItem = item.parent()
if parentItem:
if self.isModelOrChild(item):
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
else:
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
#灯光节点添加特殊处理
if self.isLightNode(nodePath):
deleteAction = menu.addAction("删除灯光")
deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item))
else:
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
def isLightNode(self, nodePath):
try:
if not nodePath or nodePath.isEmpty():
return False
# 修复:统一使用 rp_light_object
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object is not None:
return True
if hasattr(nodePath, 'getTag'):
light_type = nodePath.getTag('light_type')
if light_type in ["spot_light", "point_light"]:
return True
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
return True
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
return True
return False
except Exception as e:
print(f"判断节点是否是灯光节点失败: {str(e)}")
return False
def deleteLightNode(self, nodePath, item):
"""专门处理灯光节点的删除"""
try:
print(f"开始删除灯光节点: {nodePath.getName()}")
# 从RenderPipeline中移除灯光如果存在
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
print("从RenderPipeline移除灯光")
self.world.render_pipeline.remove_light(light_object)
nodePath.clearPythonTag('rp_light_object')
if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
print("从Spotlight列表中删除")
if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
print("从Pointlight列表中移除")
if hasattr(self.world,'selection'):
if self.world.selection.selectedNode == nodePath:
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
self.world.selection.selectedNode = None
self.world.selection.selectedObject = None
print(f"移除节点{nodePath.getName()}")
nodePath.removeNode()
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除灯光节点{nodePath.getName()}")
if hasattr(self.world,'property_panel'):
self.world.property_panel.clearPropertyPanel()
if hasattr(self.world,'selection'):
self.world.selection.updateSelection(None)
except Exception as e:
print(f"删除灯光节点失败: {str(e)}")
def _recursiveRemoveLights(self, nodePath):
"""递归删除节点及其子节点中的所有灯光"""
if nodePath.isEmpty():
return
# 先递归处理所有子节点
for child in nodePath.getChildren():
self._recursiveRemoveLights(child)
# 然后处理当前节点
if self.isLightNode(nodePath):
print(f"删除子灯光节点: {nodePath.getName()}")
# 从RenderPipeline中移除灯光
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
nodePath.clearPythonTag('rp_light_object')
# 从灯光列表中移除
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
def deleteCesiumTileset(self, nodePath, item):
"""删除 Cesium tileset"""
try:
@ -137,9 +232,14 @@ class InterfaceManager:
def deleteNode(self, nodePath, item):
"""删除节点"""
try:
item_data = item.data(0,Qt.UserRole+1)
# 如果是灯光节点,直接调用灯光删除方法
if self.isLightNode(nodePath):
self.deleteLightNode(nodePath, item)
return
item_data = item.data(0, Qt.UserRole + 1)
if item_data == "terrain":
if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains:
if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains:
terrain_to_remove = None
for terrain_info in self.world.terrain_manager.terrains:
if terrain_info['node'] == nodePath:
@ -153,34 +253,38 @@ class InterfaceManager:
self.world.selection.updateSelection(None)
return
# 先递归删除所有子节点中的灯光
self._recursiveRemoveLights(nodePath)
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
if hasattr(nodePath,'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world,'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
if hasattr(self.world,'selection'):
# 清除选择状态
if hasattr(self.world, 'selection'):
if self.world.selection.selectedNode == nodePath:
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
self.world.selection.selectedNode = None
self.world.selection.selectedObject = None
if nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
# 强制删除节点及其所有子节点
if not nodePath.isEmpty():
# 先递归删除所有子节点
children = list(nodePath.getChildren())
for child in children:
try:
if not child.isEmpty():
child.removeNode()
except Exception as e:
print(f"删除子节点失败: {str(e)}")
nodePath.removeNode()
# 再删除节点本身
nodePath.removeNode()
if hasattr(self.world,'selection'):
if hasattr(self.world, 'selection'):
self.world.selection.checkAndClearIfTargetDeleted()
# 如果是模型根节点,从模型列表中移除
#if item.parent().text(0) == "模型":
if nodePath in self.world.models:
if hasattr(self.world, 'models') and nodePath in self.world.models:
self.world.models.remove(nodePath)
# 从树形控件中移除
@ -196,6 +300,51 @@ class InterfaceManager:
except Exception as e:
print(f"删除节点失败: {str(e)}")
import traceback
traceback.print_exc()
def _cleanupAllLightsInSubtree(self,parentNode):
try:
if parentNode.isEmpty():
return
#收集所有子节点
all_nodes = []
def collect_all_nodes(node):
if not node.isEmpty():
all_nodes.append(node)
for child in node.getChildren():
collect_all_nodes(child)
collect_all_nodes(parentNode)
ligths_processed = 0
for node in all_nodes:
if self.isLightNode(node):
if hasattr(node,'getPythonTag'):
light_object = node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world,'render_pipeline'):
try:
self.world.render_pipeline.remove_light(light_object)
print(f"✓ 从渲染管线移除灯光: {node.getName()}")
except Exception as e:
print(f"⚠ 从渲染管线移除灯光失败: {str(e)}")
#从灯光列表中移除
try:
if node in self.world.Spotlight:
self.world.Spotlight.remove(node)
print(f"从Spotlight列表移除{node.getName()}")
if node in self.world.Pointlight:
self.world.Pointlight.remove(node)
print(f"从pointlight列表移除{node.getName()}")
except Exception as e:
print(f"从灯列表移除失败{str(e)}")
ligths_processed += 1
if ligths_processed>0:
print(f"清理{ligths_processed}个灯光节点")
except Exception as e:
print(f"清理节点树中的灯光失败{str(e)}")
def updateSceneTree(self):
"""更新场景树显示 - 实际实现"""

View File

@ -314,6 +314,20 @@ class MainWindow(QMainWindow):
self.createMenu = menubar.addMenu('创建')
self.setupCreateMenuActions() # 统一创建菜单动作
# self.createGUIaddMenu = self.createMenu.addMenu('GUI')
# self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
# self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
# self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
# self.createGUIaddMenu.addSeparator()
# self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
# self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图')
# self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图')
# self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图')
#
# self.createLightaddMenu = self.createMenu.addMenu('光源')
# self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
# self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
#添加地形菜单
self.createTerrainMenu = self.createMenu.addMenu('地形')
self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形')
@ -647,11 +661,6 @@ class MainWindow(QMainWindow):
self.createHeightmapTerrainTool.setText("高度图地形")
self.toolbar.addWidget(self.createHeightmapTerrainTool)
self.terrainEditTool = QToolButton()
self.terrainEditTool.setText("地形编辑")
self.terrainEditTool.setCheckable(True)
self.toolbar.addWidget(self.terrainEditTool)
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
@ -1429,22 +1438,6 @@ class MainWindow(QMainWindow):
else:
QMessageBox.warning(self, "错误", "高度图地形创建失败!")
def onTerrainEditMode(self):
"""地形编辑模式"""
# 检查当前是否已经处于地形编辑模式
if self.world.currentTool == "地形编辑":
# 退出地形编辑模式
self.world.setCurrentTool(None)
self.terrainEditTool.setChecked(False)
self.terrainEditTool.setText("地形编辑")
QMessageBox.information(self, "地形编辑", "已退出地形编辑模式")
else:
# 进入地形编辑模式
self.world.setCurrentTool("地形编辑")
self.terrainEditTool.setChecked(True)
self.terrainEditTool.setText("退出地形编辑")
QMessageBox.information(self, "地形编辑",
"已进入地形编辑模式\n\n使用鼠标左键抬高地形\n使用鼠标右键降低地形")
def setup_main_window(world):
"""设置主窗口的便利函数"""
app = QApplication.instance()

View File

@ -340,9 +340,9 @@ class PropertyPanelManager:
self.scale_y.setValue(scale.getY())
self.scale_z.setValue(scale.getZ())
self.scale_x.valueChanged.connect(lambda v:terrain_node.setScaleX(v))
self.scale_y.valueChanged.connect(lambda v:terrain_node.setScaleY(v))
self.scale_z.valueChanged.connect(lambda v:terrain_node.setScaleZ(v))
self.scale_x.valueChanged.connect(lambda v:self._updateXScale(terrain_node,v))
self.scale_y.valueChanged.connect(lambda v:self._updateYScale(terrain_node,v))
self.scale_z.valueChanged.connect(lambda v:self._updateZScale(terrain_node,v))
x_label2 = QLabel("X")
y_label2 = QLabel("Y")
@ -1153,9 +1153,6 @@ class PropertyPanelManager:
current_scale.getZ()])):
scale_widget.setRange(-1000, 1000)
scale_widget.setSingleStep(0.1)
# 如果缩放值为0设置为一个很小的非零值
if scale_value == 0:
scale_value = 0.01 if scale_value >= 0 else -0.01
scale_widget.setValue(scale_value)
self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value))
@ -1193,13 +1190,7 @@ class PropertyPanelManager:
self._updateModelMaterialPanel(model)
def _onScaleValueChanged(self, scale_widget, value):
"""确保缩放值不为0"""
if value == 0:
# 设置为一个很小的非零值,保持原有符号
if hasattr(scale_widget, 'value') and scale_widget.value() > 0:
scale_widget.setValue(0.01)
else:
scale_widget.setValue(-0.01)
pass
def _updateXScale(self, model, value):
"""更新X轴缩放值"""
@ -1558,6 +1549,7 @@ class PropertyPanelManager:
if success:
# 保存路径到 Tag
gui_element.setTag("texture_path", file_path)
gui_element.setTag("gui_image_path",file_path)
# 更新显示
texture_label.setText(file_path)
# 可选:刷新场景树或其他 UI
@ -1925,19 +1917,6 @@ class PropertyPanelManager:
except Exception as e:
print(f"✗ 更新GUI元素Z轴缩放失败: {e}")
def update3DImageTexture(self,nodepath,texture_path):
try:
tex = self.world.loader.loadTexture(texture_path)
if tex:
nodepath.setTexture(tex,1)
return True
else:
print(f"[警告] 无法加载贴图: {texture_path}")
return False
except Exception as e:
print(f"[错误] 更新 3D 图片纹理失败: {e}")
return False
def update2DImageTexture(self, gui_element, image_path):
try:
new_texture = self.world.loader.loadTexture(image_path)

View File

@ -20,7 +20,7 @@ from PyQt5.sip import wrapinstance
from panda3d.core import ModelRoot
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from scene import util
class NewProjectDialog(QDialog):
"""新建项目对话框"""
@ -150,8 +150,8 @@ class CustomPanda3DWidget(QPanda3DWidget):
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
# 使用关键字参数确保兼容性
self.world.importModel(filepath)
#self.world.addAnimationPanel(None,filepath)
event.acceptProposedAction()
else:
event.ignore()
@ -936,6 +936,7 @@ class CustomAssetsTreeWidget(QTreeWidget):
internal_paths.append(filepath)
# 检查是否是模型文件(用于向外拖拽)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
print(f"模型路ing{QUrl.fromLocalFile(filepath)}")
urls.append(QUrl.fromLocalFile(filepath))
# 设置内部拖拽数据
@ -1910,24 +1911,30 @@ class CustomTreeWidget(QTreeWidget):
def should_skip(node):
name = node.getName()
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name==""
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
node.node(), ModelRoot) or name == ""
def addNodeToTree(node,parentItem,force = False):
def addNodeToTree(node, parentItem, force=False):
if not force and should_skip(node):
return
nodeItem = QTreeWidgetItem(parentItem,[node.getName()])
nodeItem.setData(0,Qt.UserRole, node)
return None
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
nodeItem.setData(0, Qt.UserRole + 1, node_type)
for child in node.getChildren():
addNodeToTree(child,nodeItem,force = False)
addNodeToTree(child, nodeItem, force=False)
return nodeItem
try:
from PyQt5.QtWidgets import QTreeWidgetItem
from PyQt5.QtCore import Qt
# 初始化new_qt_item变量
new_qt_item = None
if node_type == "IMPORTED_MODEL_NODE":
node_name = node.getTag("file") if hasattr(node, 'getTag') else "model"
addNodeToTree(node, parent_item, force=True)
new_qt_item = addNodeToTree(node, parent_item, force=True)
else:
node_name = node.getName() if hasattr(node, 'getName') else "node"
new_qt_item = QTreeWidgetItem(parent_item, [node_name])